Add product features, Heleket crypto payments and order status icons.
This commit is contained in:
+156
-25
@@ -8,6 +8,8 @@ import (
|
||||
"strconv"
|
||||
|
||||
"shop/internal/auth"
|
||||
"shop/internal/compare"
|
||||
"shop/internal/heleket"
|
||||
"shop/internal/models"
|
||||
"shop/internal/repository"
|
||||
"shop/internal/version"
|
||||
@@ -18,18 +20,26 @@ type CustomerHandler struct {
|
||||
categories *repository.CategoryRepository
|
||||
users *repository.UserRepository
|
||||
cart *repository.CartRepository
|
||||
orders *repository.OrderRepository
|
||||
stats *repository.StatsRepository
|
||||
userSession *auth.Session
|
||||
cartSession *auth.Session
|
||||
templates *template.Template
|
||||
orders *repository.OrderRepository
|
||||
priceHistory *repository.PriceHistoryRepository
|
||||
reservations *repository.ReservationRepository
|
||||
stats *repository.StatsRepository
|
||||
userSession *auth.Session
|
||||
cartSession *auth.Session
|
||||
compareSession *auth.Session
|
||||
heleket *heleket.Client
|
||||
heleketAPIKey string
|
||||
publicBaseURL string
|
||||
heleketCurrency string
|
||||
templates *template.Template
|
||||
}
|
||||
|
||||
type shopPage struct {
|
||||
Title string
|
||||
User *models.User
|
||||
CartCount int
|
||||
Version string
|
||||
CartCount int
|
||||
CompareCount int
|
||||
Version string
|
||||
Products interface{}
|
||||
Categories interface{}
|
||||
Error string
|
||||
@@ -44,12 +54,13 @@ type cartPage struct {
|
||||
|
||||
type checkoutPage struct {
|
||||
shopPage
|
||||
Items []models.CartItem
|
||||
Total float64
|
||||
Name string
|
||||
Email string
|
||||
Phone string
|
||||
Address string
|
||||
Items []models.CartItem
|
||||
Total float64
|
||||
Name string
|
||||
Email string
|
||||
Phone string
|
||||
Address string
|
||||
HeleketEnabled bool
|
||||
}
|
||||
|
||||
type accountPage struct {
|
||||
@@ -62,7 +73,20 @@ type accountPage struct {
|
||||
|
||||
type productPage struct {
|
||||
shopPage
|
||||
Product models.Product
|
||||
Product models.Product
|
||||
PriceHistory []models.PriceHistoryEntry
|
||||
MaxHistoryPrice float64
|
||||
Reservation *models.Reservation
|
||||
ReservedByOther bool
|
||||
CompareProducts []models.Product
|
||||
CompareCount int
|
||||
Success string
|
||||
Error string
|
||||
}
|
||||
|
||||
type comparePage struct {
|
||||
shopPage
|
||||
Products []models.Product
|
||||
}
|
||||
|
||||
type categoryPage struct {
|
||||
@@ -77,13 +101,21 @@ func NewCustomerHandler(
|
||||
users *repository.UserRepository,
|
||||
cart *repository.CartRepository,
|
||||
orders *repository.OrderRepository,
|
||||
priceHistory *repository.PriceHistoryRepository,
|
||||
reservations *repository.ReservationRepository,
|
||||
stats *repository.StatsRepository,
|
||||
userSession, cartSession *auth.Session,
|
||||
userSession, cartSession, compareSession *auth.Session,
|
||||
heleketClient *heleket.Client,
|
||||
heleketAPIKey, publicBaseURL, heleketCurrency string,
|
||||
templates *template.Template,
|
||||
) *CustomerHandler {
|
||||
return &CustomerHandler{
|
||||
products: products, categories: categories, users: users, cart: cart, orders: orders, stats: stats,
|
||||
userSession: userSession, cartSession: cartSession, templates: templates,
|
||||
products: products, categories: categories, users: users, cart: cart, orders: orders,
|
||||
priceHistory: priceHistory, reservations: reservations, stats: stats,
|
||||
userSession: userSession, cartSession: cartSession, compareSession: compareSession,
|
||||
heleket: heleketClient, heleketAPIKey: heleketAPIKey,
|
||||
publicBaseURL: publicBaseURL, heleketCurrency: heleketCurrency,
|
||||
templates: templates,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +140,7 @@ func (h *CustomerHandler) basePage(r *http.Request, w http.ResponseWriter, title
|
||||
p.CartCount = n
|
||||
}
|
||||
}
|
||||
p.CompareCount = h.compareCount(r)
|
||||
return p
|
||||
}
|
||||
|
||||
@@ -140,15 +173,64 @@ func (h *CustomerHandler) ProductPage(w http.ResponseWriter, r *http.Request) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
product, err := h.products.GetActiveByID(r.Context(), id)
|
||||
ctx := r.Context()
|
||||
product, err := h.products.GetActiveByID(ctx, id)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
p := productPage{
|
||||
shopPage: h.basePage(r, w, product.Name),
|
||||
Product: product,
|
||||
}
|
||||
p.PriceHistory, _ = h.priceHistory.ByProduct(ctx, id, 12)
|
||||
for _, e := range p.PriceHistory {
|
||||
if e.Price > p.MaxHistoryPrice {
|
||||
p.MaxHistoryPrice = e.Price
|
||||
}
|
||||
}
|
||||
|
||||
sessionKey := h.cartSession.EnsureKey(w, r)
|
||||
if res, err := h.reservations.ActiveByProduct(ctx, id); err == nil {
|
||||
p.Reservation = res
|
||||
if res.SessionKey != sessionKey {
|
||||
var uid int
|
||||
if u, ok := auth.UserIDFromSession(h.userSession, r); ok {
|
||||
uid = u
|
||||
}
|
||||
if res.UserID == nil || uid == 0 || *res.UserID != uid {
|
||||
p.ReservedByOther = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compareRaw, _ := h.compareSession.FromRequest(r)
|
||||
compareIDs := compare.Parse(compareRaw)
|
||||
p.CompareCount = len(compareIDs)
|
||||
|
||||
var others []models.Product
|
||||
if product.CategoryID != nil {
|
||||
others, _ = h.products.ByCategoryID(ctx, *product.CategoryID)
|
||||
} else {
|
||||
others, _ = h.products.Featured(ctx, 50)
|
||||
}
|
||||
for _, o := range others {
|
||||
if o.ID != id {
|
||||
p.CompareProducts = append(p.CompareProducts, o)
|
||||
}
|
||||
}
|
||||
|
||||
switch r.URL.Query().Get("ok") {
|
||||
case "reserve":
|
||||
p.Success = "Товар забронирован на 24 часа"
|
||||
case "compare":
|
||||
p.Success = "Товар добавлен к сравнению"
|
||||
}
|
||||
if r.URL.Query().Get("err") == "reserved" {
|
||||
p.Error = "Товар уже забронирован другим покупателем"
|
||||
}
|
||||
|
||||
h.render(w, "product.html", p)
|
||||
}
|
||||
|
||||
@@ -321,9 +403,10 @@ func (h *CustomerHandler) CheckoutPage(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
total, _ := h.cart.Total(ctx, cartID)
|
||||
p := checkoutPage{
|
||||
shopPage: h.basePage(r, w, "Оформление заказа"),
|
||||
Items: items,
|
||||
Total: total,
|
||||
shopPage: h.basePage(r, w, "Оформление заказа"),
|
||||
Items: items,
|
||||
Total: total,
|
||||
HeleketEnabled: h.heleket.Enabled(),
|
||||
}
|
||||
if p.User != nil {
|
||||
p.Name = p.User.Name
|
||||
@@ -354,9 +437,14 @@ func (h *CustomerHandler) Checkout(w http.ResponseWriter, r *http.Request) {
|
||||
address := r.FormValue("address")
|
||||
|
||||
p := checkoutPage{
|
||||
shopPage: h.basePage(r, w, "Оформление заказа"),
|
||||
Items: items, Total: total,
|
||||
Name: name, Email: email, Phone: phone, Address: address,
|
||||
shopPage: h.basePage(r, w, "Оформление заказа"),
|
||||
Items: items,
|
||||
Total: total,
|
||||
Name: name,
|
||||
Email: email,
|
||||
Phone: phone,
|
||||
Address: address,
|
||||
HeleketEnabled: h.heleket.Enabled(),
|
||||
}
|
||||
if name == "" || email == "" || phone == "" || address == "" {
|
||||
p.Error = "Заполните все поля доставки"
|
||||
@@ -364,9 +452,22 @@ func (h *CustomerHandler) Checkout(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
paymentMethod := r.FormValue("payment_method")
|
||||
if paymentMethod != "heleket" {
|
||||
paymentMethod = "cod"
|
||||
}
|
||||
if paymentMethod == "heleket" && !h.heleket.Enabled() {
|
||||
p.Error = "Онлайн-оплата временно недоступна"
|
||||
h.render(w, "checkout.html", p)
|
||||
return
|
||||
}
|
||||
|
||||
order := models.Order{
|
||||
GuestName: name, GuestEmail: email, GuestPhone: phone,
|
||||
Address: address, Total: total,
|
||||
Address: address, Total: total, PaymentMethod: paymentMethod,
|
||||
}
|
||||
if paymentMethod == "heleket" {
|
||||
order.Status = "unpaid"
|
||||
}
|
||||
if uid, ok := auth.UserIDFromSession(h.userSession, r); ok {
|
||||
order.UserID = &uid
|
||||
@@ -379,6 +480,36 @@ func (h *CustomerHandler) Checkout(w http.ResponseWriter, r *http.Request) {
|
||||
h.render(w, "checkout.html", p)
|
||||
return
|
||||
}
|
||||
|
||||
if paymentMethod == "heleket" {
|
||||
externalID := heleket.OrderExternalID(order.ID)
|
||||
base := h.publicBaseURL
|
||||
if base == "" {
|
||||
base = "http://" + r.Host
|
||||
}
|
||||
invoice, err := h.heleket.CreateInvoice(ctx, heleket.InvoiceRequest{
|
||||
Amount: heleket.FormatAmount(total),
|
||||
Currency: h.heleketCurrency,
|
||||
OrderID: externalID,
|
||||
URLReturn: base + "/cart",
|
||||
URLSuccess: base + "/order/" + strconv.Itoa(order.ID) + "/success",
|
||||
URLCallback: base + "/webhooks/heleket",
|
||||
PayerEmail: email,
|
||||
Lifetime: 3600,
|
||||
AdditionalData: "order:" + strconv.Itoa(order.ID),
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("heleket invoice: %v", err)
|
||||
p.Error = "Заказ #" + strconv.Itoa(order.ID) + " создан, но счёт не выдан. Попробуйте оплатить из личного кабинета."
|
||||
h.render(w, "checkout.html", p)
|
||||
return
|
||||
}
|
||||
_ = h.orders.UpdateHeleketPayment(ctx, order.ID, invoice.UUID, externalID, invoice.URL, invoice.PaymentStatus)
|
||||
_ = h.cart.Clear(ctx, cartID)
|
||||
http.Redirect(w, r, invoice.URL, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
_ = h.cart.Clear(ctx, cartID)
|
||||
|
||||
success := h.basePage(r, w, "Заказ оформлен")
|
||||
|
||||
Reference in New Issue
Block a user