Add product features, Heleket crypto payments and order status icons.

This commit is contained in:
shop
2026-06-27 16:29:33 +03:00
parent 361d35e6b4
commit 98a0bb01e2
31 changed files with 1673 additions and 126 deletions
+156 -25
View File
@@ -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, "Заказ оформлен")