The ad service now returns ads matching the categories of the product that is currently displayed. Changes in this commit: - List all products' categories in products.json. - Pass the current product's categories from the frontend to the ad service when looking up ads. - Store a statically initialized multimap from product category to ad in the ad service. - Return all ads matching the given categories when handling an ads request. The ad service continues to return random ads when no categories are given or no ads match the categories.
397 lines
13 KiB
Go
397 lines
13 KiB
Go
// Copyright 2018 Google LLC
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"html/template"
|
|
"math/rand"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
|
|
pb "github.com/GoogleCloudPlatform/microservices-demo/src/frontend/genproto"
|
|
"github.com/GoogleCloudPlatform/microservices-demo/src/frontend/money"
|
|
)
|
|
|
|
var (
|
|
templates = template.Must(template.New("").
|
|
Funcs(template.FuncMap{
|
|
"renderMoney": renderMoney,
|
|
}).ParseGlob("templates/*.html"))
|
|
)
|
|
|
|
func (fe *frontendServer) homeHandler(w http.ResponseWriter, r *http.Request) {
|
|
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
|
|
log.WithField("currency", currentCurrency(r)).Info("home")
|
|
currencies, err := fe.getCurrencies(r.Context())
|
|
if err != nil {
|
|
renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve currencies"), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
products, err := fe.getProducts(r.Context())
|
|
if err != nil {
|
|
renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve products"), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
cart, err := fe.getCart(r.Context(), sessionID(r))
|
|
if err != nil {
|
|
renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve cart"), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
type productView struct {
|
|
Item *pb.Product
|
|
Price *pb.Money
|
|
}
|
|
ps := make([]productView, len(products))
|
|
for i, p := range products {
|
|
price, err := fe.convertCurrency(r.Context(), p.GetPriceUsd(), currentCurrency(r))
|
|
if err != nil {
|
|
renderHTTPError(log, r, w, errors.Wrapf(err, "failed to do currency conversion for product %s", p.GetId()), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
ps[i] = productView{p, price}
|
|
}
|
|
|
|
if err := templates.ExecuteTemplate(w, "home", map[string]interface{}{
|
|
"session_id": sessionID(r),
|
|
"request_id": r.Context().Value(ctxKeyRequestID{}),
|
|
"user_currency": currentCurrency(r),
|
|
"currencies": currencies,
|
|
"products": ps,
|
|
"cart_size": len(cart),
|
|
"banner_color": os.Getenv("BANNER_COLOR"), // illustrates canary deployments
|
|
"ad": fe.chooseAd(r.Context(), []string{}, log),
|
|
}); err != nil {
|
|
log.Error(err)
|
|
}
|
|
}
|
|
|
|
func (fe *frontendServer) productHandler(w http.ResponseWriter, r *http.Request) {
|
|
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
|
|
id := mux.Vars(r)["id"]
|
|
if id == "" {
|
|
renderHTTPError(log, r, w, errors.New("product id not specified"), http.StatusBadRequest)
|
|
return
|
|
}
|
|
log.WithField("id", id).WithField("currency", currentCurrency(r)).
|
|
Debug("serving product page")
|
|
|
|
p, err := fe.getProduct(r.Context(), id)
|
|
if err != nil {
|
|
renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve product"), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
currencies, err := fe.getCurrencies(r.Context())
|
|
if err != nil {
|
|
renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve currencies"), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
cart, err := fe.getCart(r.Context(), sessionID(r))
|
|
if err != nil {
|
|
renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve cart"), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
price, err := fe.convertCurrency(r.Context(), p.GetPriceUsd(), currentCurrency(r))
|
|
if err != nil {
|
|
renderHTTPError(log, r, w, errors.Wrap(err, "failed to convert currency"), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
recommendations, err := fe.getRecommendations(r.Context(), sessionID(r), []string{id})
|
|
if err != nil {
|
|
renderHTTPError(log, r, w, errors.Wrap(err, "failed to get product recommendations"), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
product := struct {
|
|
Item *pb.Product
|
|
Price *pb.Money
|
|
}{p, price}
|
|
|
|
if err := templates.ExecuteTemplate(w, "product", map[string]interface{}{
|
|
"session_id": sessionID(r),
|
|
"request_id": r.Context().Value(ctxKeyRequestID{}),
|
|
"ad": fe.chooseAd(r.Context(), p.Categories, log),
|
|
"user_currency": currentCurrency(r),
|
|
"currencies": currencies,
|
|
"product": product,
|
|
"recommendations": recommendations,
|
|
"cart_size": len(cart),
|
|
}); err != nil {
|
|
log.Println(err)
|
|
}
|
|
}
|
|
|
|
func (fe *frontendServer) addToCartHandler(w http.ResponseWriter, r *http.Request) {
|
|
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
|
|
quantity, _ := strconv.ParseUint(r.FormValue("quantity"), 10, 32)
|
|
productID := r.FormValue("product_id")
|
|
if productID == "" || quantity == 0 {
|
|
renderHTTPError(log, r, w, errors.New("invalid form input"), http.StatusBadRequest)
|
|
return
|
|
}
|
|
log.WithField("product", productID).WithField("quantity", quantity).Debug("adding to cart")
|
|
|
|
p, err := fe.getProduct(r.Context(), productID)
|
|
if err != nil {
|
|
renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve product"), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if err := fe.insertCart(r.Context(), sessionID(r), p.GetId(), int32(quantity)); err != nil {
|
|
renderHTTPError(log, r, w, errors.Wrap(err, "failed to add to cart"), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("location", "/cart")
|
|
w.WriteHeader(http.StatusFound)
|
|
}
|
|
|
|
func (fe *frontendServer) emptyCartHandler(w http.ResponseWriter, r *http.Request) {
|
|
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
|
|
log.Debug("emptying cart")
|
|
|
|
if err := fe.emptyCart(r.Context(), sessionID(r)); err != nil {
|
|
renderHTTPError(log, r, w, errors.Wrap(err, "failed to empty cart"), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("location", "/")
|
|
w.WriteHeader(http.StatusFound)
|
|
}
|
|
|
|
func (fe *frontendServer) viewCartHandler(w http.ResponseWriter, r *http.Request) {
|
|
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
|
|
log.Debug("view user cart")
|
|
currencies, err := fe.getCurrencies(r.Context())
|
|
if err != nil {
|
|
renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve currencies"), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
cart, err := fe.getCart(r.Context(), sessionID(r))
|
|
if err != nil {
|
|
renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve cart"), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
recommendations, err := fe.getRecommendations(r.Context(), sessionID(r), cartIDs(cart))
|
|
if err != nil {
|
|
renderHTTPError(log, r, w, errors.Wrap(err, "failed to get product recommendations"), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
shippingCost, err := fe.getShippingQuote(r.Context(), cart, currentCurrency(r))
|
|
if err != nil {
|
|
renderHTTPError(log, r, w, errors.Wrap(err, "failed to get shipping quote"), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
type cartItemView struct {
|
|
Item *pb.Product
|
|
Quantity int32
|
|
Price *pb.Money
|
|
}
|
|
items := make([]cartItemView, len(cart))
|
|
totalPrice := pb.Money{CurrencyCode: currentCurrency(r)}
|
|
for i, item := range cart {
|
|
p, err := fe.getProduct(r.Context(), item.GetProductId())
|
|
if err != nil {
|
|
renderHTTPError(log, r, w, errors.Wrapf(err, "could not retrieve product #%s", item.GetProductId()), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
price, err := fe.convertCurrency(r.Context(), p.GetPriceUsd(), currentCurrency(r))
|
|
if err != nil {
|
|
renderHTTPError(log, r, w, errors.Wrapf(err, "could not convert currency for product #%s", item.GetProductId()), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
multPrice := money.MultiplySlow(*price, uint32(item.GetQuantity()))
|
|
items[i] = cartItemView{
|
|
Item: p,
|
|
Quantity: item.GetQuantity(),
|
|
Price: &multPrice}
|
|
totalPrice = money.Must(money.Sum(totalPrice, multPrice))
|
|
}
|
|
totalPrice = money.Must(money.Sum(totalPrice, *shippingCost))
|
|
|
|
year := time.Now().Year()
|
|
if err := templates.ExecuteTemplate(w, "cart", map[string]interface{}{
|
|
"session_id": sessionID(r),
|
|
"request_id": r.Context().Value(ctxKeyRequestID{}),
|
|
"user_currency": currentCurrency(r),
|
|
"currencies": currencies,
|
|
"recommendations": recommendations,
|
|
"cart_size": len(cart),
|
|
"shipping_cost": shippingCost,
|
|
"total_cost": totalPrice,
|
|
"items": items,
|
|
"expiration_years": []int{year, year + 1, year + 2, year + 3, year + 4},
|
|
}); err != nil {
|
|
log.Println(err)
|
|
}
|
|
}
|
|
|
|
func (fe *frontendServer) placeOrderHandler(w http.ResponseWriter, r *http.Request) {
|
|
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
|
|
log.Debug("placing order")
|
|
|
|
var (
|
|
email = r.FormValue("email")
|
|
streetAddress = r.FormValue("street_address")
|
|
zipCode, _ = strconv.ParseInt(r.FormValue("zip_code"), 10, 32)
|
|
city = r.FormValue("city")
|
|
state = r.FormValue("state")
|
|
country = r.FormValue("country")
|
|
ccNumber = r.FormValue("credit_card_number")
|
|
ccMonth, _ = strconv.ParseInt(r.FormValue("credit_card_expiration_month"), 10, 32)
|
|
ccYear, _ = strconv.ParseInt(r.FormValue("credit_card_expiration_year"), 10, 32)
|
|
ccCVV, _ = strconv.ParseInt(r.FormValue("credit_card_cvv"), 10, 32)
|
|
)
|
|
|
|
order, err := pb.NewCheckoutServiceClient(fe.checkoutSvcConn).
|
|
PlaceOrder(r.Context(), &pb.PlaceOrderRequest{
|
|
Email: email,
|
|
CreditCard: &pb.CreditCardInfo{
|
|
CreditCardNumber: ccNumber,
|
|
CreditCardExpirationMonth: int32(ccMonth),
|
|
CreditCardExpirationYear: int32(ccYear),
|
|
CreditCardCvv: int32(ccCVV)},
|
|
UserId: sessionID(r),
|
|
UserCurrency: currentCurrency(r),
|
|
Address: &pb.Address{
|
|
StreetAddress: streetAddress,
|
|
City: city,
|
|
State: state,
|
|
ZipCode: int32(zipCode),
|
|
Country: country},
|
|
})
|
|
if err != nil {
|
|
renderHTTPError(log, r, w, errors.Wrap(err, "failed to complete the order"), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
log.WithField("order", order.GetOrder().GetOrderId()).Info("order placed")
|
|
|
|
order.GetOrder().GetItems()
|
|
recommendations, _ := fe.getRecommendations(r.Context(), sessionID(r), nil)
|
|
|
|
totalPaid := *order.GetOrder().GetShippingCost()
|
|
for _, v := range order.GetOrder().GetItems() {
|
|
totalPaid = money.Must(money.Sum(totalPaid, *v.GetCost()))
|
|
}
|
|
|
|
if err := templates.ExecuteTemplate(w, "order", map[string]interface{}{
|
|
"session_id": sessionID(r),
|
|
"request_id": r.Context().Value(ctxKeyRequestID{}),
|
|
"user_currency": currentCurrency(r),
|
|
"order": order.GetOrder(),
|
|
"total_paid": &totalPaid,
|
|
"recommendations": recommendations,
|
|
}); err != nil {
|
|
log.Println(err)
|
|
}
|
|
}
|
|
|
|
func (fe *frontendServer) logoutHandler(w http.ResponseWriter, r *http.Request) {
|
|
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
|
|
log.Debug("logging out")
|
|
for _, c := range r.Cookies() {
|
|
c.Expires = time.Now().Add(-time.Hour * 24 * 365)
|
|
c.MaxAge = -1
|
|
http.SetCookie(w, c)
|
|
}
|
|
w.Header().Set("Location", "/")
|
|
w.WriteHeader(http.StatusFound)
|
|
}
|
|
|
|
func (fe *frontendServer) setCurrencyHandler(w http.ResponseWriter, r *http.Request) {
|
|
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
|
|
cur := r.FormValue("currency_code")
|
|
log.WithField("curr.new", cur).WithField("curr.old", currentCurrency(r)).
|
|
Debug("setting currency")
|
|
|
|
if cur != "" {
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: cookieCurrency,
|
|
Value: cur,
|
|
MaxAge: cookieMaxAge,
|
|
})
|
|
}
|
|
referer := r.Header.Get("referer")
|
|
if referer == "" {
|
|
referer = "/"
|
|
}
|
|
w.Header().Set("Location", referer)
|
|
w.WriteHeader(http.StatusFound)
|
|
}
|
|
|
|
// chooseAd queries for advertisements available and randomly chooses one, if
|
|
// available. It ignores the error retrieving the ad since it is not critical.
|
|
func (fe *frontendServer) chooseAd(ctx context.Context, ctxKeys []string, log logrus.FieldLogger) *pb.Ad {
|
|
ads, err := fe.getAd(ctx, ctxKeys)
|
|
if err != nil {
|
|
log.WithField("error", err).Warn("failed to retrieve ads")
|
|
return nil
|
|
}
|
|
return ads[rand.Intn(len(ads))]
|
|
}
|
|
|
|
func renderHTTPError(log logrus.FieldLogger, r *http.Request, w http.ResponseWriter, err error, code int) {
|
|
log.WithField("error", err).Error("request error")
|
|
errMsg := fmt.Sprintf("%+v", err)
|
|
|
|
w.WriteHeader(code)
|
|
templates.ExecuteTemplate(w, "error", map[string]interface{}{
|
|
"session_id": sessionID(r),
|
|
"request_id": r.Context().Value(ctxKeyRequestID{}),
|
|
"error": errMsg,
|
|
"status_code": code,
|
|
"status": http.StatusText(code)})
|
|
}
|
|
|
|
func currentCurrency(r *http.Request) string {
|
|
c, _ := r.Cookie(cookieCurrency)
|
|
if c != nil {
|
|
return c.Value
|
|
}
|
|
return defaultCurrency
|
|
}
|
|
|
|
func sessionID(r *http.Request) string {
|
|
v := r.Context().Value(ctxKeySessionID{})
|
|
if v != nil {
|
|
return v.(string)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func cartIDs(c []*pb.CartItem) []string {
|
|
out := make([]string, len(c))
|
|
for i, v := range c {
|
|
out[i] = v.GetProductId()
|
|
}
|
|
return out
|
|
}
|
|
|
|
func renderMoney(money pb.Money) string {
|
|
return fmt.Sprintf("%s %d.%02d", money.GetCurrencyCode(), money.GetUnits(), money.GetNanos()/10000000)
|
|
}
|