almost all ready

This commit is contained in:
2026-05-14 01:48:15 +02:00
parent 1b53c1c199
commit d55b0e2914
29 changed files with 5489 additions and 650 deletions
+735
View File
@@ -2,7 +2,12 @@ package cart
import (
"context"
"database/sql"
"errors"
"fmt"
"strconv"
"strings"
"time"
"gorm.io/gorm"
)
@@ -12,6 +17,63 @@ type Summary struct {
TotalItems int64
}
type Page struct {
ID int64
Items []Item
TotalItems int64
Subtotal float64
SubtotalTaxIncl float64
}
type Item struct {
ProductID int64
ProductAttributeID int64
CustomizationID int64
Name string
Slug string
CategoryPath string
EAN13 string
CoverImageID sql.NullInt64 `gorm:"column:cover_image_id"`
ImageURL string `gorm:"-"`
Quantity int64
UnitPrice float64
UnitPriceTaxIncl float64 `gorm:"column:unit_price_tax_incl"`
LineTotal float64
LineTotalTaxIncl float64 `gorm:"column:line_total_tax_incl"`
TaxRate float64 `gorm:"column:tax_rate"`
CurrencyID int64 `gorm:"column:currency_id"`
CurrencyCode string `gorm:"column:currency_code"`
CurrencySign string `gorm:"column:currency_sign"`
ConversionRate float64 `gorm:"column:conversion_rate"`
URL string `gorm:"-"`
Attributes []ItemAttribute `gorm:"-"`
}
type ItemAttribute struct {
Group string
Value string
}
type MutationInput struct {
CartID int64
ProductID int64
ProductAttributeID int64
CustomizationID int64
Quantity int64
CustomerID int64
GuestID int64
LanguageID int64
CurrencyID int64
ShopID int64
}
type MutationResult struct {
CartID int64
LineQuantity int64
TotalItems int64
CreatedCart bool
}
type Service struct {
db *gorm.DB
prefix string
@@ -33,3 +95,676 @@ func (s *Service) SummaryByID(ctx context.Context, cartID int64) (*Summary, erro
}
return &summary, nil
}
func (s *Service) PageByID(ctx context.Context, cartID, languageID, shopID, currencyID int64) (*Page, error) {
if s == nil || s.db == nil {
return nil, errors.New("prestashop cart service is not initialized")
}
if cartID == 0 {
return &Page{}, nil
}
if languageID == 0 {
languageID = 1
}
if shopID == 0 {
shopID = 1
}
if currencyID == 0 {
currencyID = 1
}
query := fmt.Sprintf(`
SELECT cp.id_product AS product_id,
COALESCE(cp.id_product_attribute, 0) AS product_attribute_id,
COALESCE(cp.id_customization, 0) AS customization_id,
pl.name AS name,
pl.link_rewrite AS slug,
cl.link_rewrite AS category_path,
p.ean13 AS ean13,
COALESCE(combination_image.id_image, i.id_image) AS cover_image_id,
cp.quantity AS quantity,
((product_shop.price + COALESCE(product_attribute_shop.price, 0)) * curr.conversion_rate) AS unit_price,
(((product_shop.price + COALESCE(product_attribute_shop.price, 0)) * curr.conversion_rate) * (1 + COALESCE(tax_data.tax_rate, 0) / 100)) AS unit_price_tax_incl,
(cp.quantity * ((product_shop.price + COALESCE(product_attribute_shop.price, 0)) * curr.conversion_rate)) AS line_total,
(cp.quantity * (((product_shop.price + COALESCE(product_attribute_shop.price, 0)) * curr.conversion_rate) * (1 + COALESCE(tax_data.tax_rate, 0) / 100))) AS line_total_tax_incl,
COALESCE(tax_data.tax_rate, 0) AS tax_rate,
curr.id_currency AS currency_id,
curr.iso_code AS currency_code,
curr.sign AS currency_sign,
curr.conversion_rate AS conversion_rate
FROM %scart_product cp
JOIN %sproduct p ON p.id_product = cp.id_product
JOIN %sproduct_shop product_shop
ON product_shop.id_product = cp.id_product
AND product_shop.id_shop = cp.id_shop
JOIN %sproduct_lang pl
ON pl.id_product = cp.id_product
AND pl.id_lang = ?
AND pl.id_shop = ?
LEFT JOIN %simage i ON i.id_product = cp.id_product AND i.cover = 1
LEFT JOIN (
SELECT pai.id_product_attribute,
MIN(pai.id_image) AS id_image
FROM %sproduct_attribute_image pai
GROUP BY pai.id_product_attribute
) combination_image
ON combination_image.id_product_attribute = cp.id_product_attribute
LEFT JOIN %sproduct_attribute_shop product_attribute_shop
ON product_attribute_shop.id_product_attribute = cp.id_product_attribute
AND product_attribute_shop.id_shop = cp.id_shop
JOIN %scurrency curr ON curr.id_currency = ? AND curr.deleted = 0
LEFT JOIN (
SELECT tr.id_tax_rules_group,
SUM(t.rate) AS tax_rate
FROM %stax_rule tr
JOIN %stax t ON t.id_tax = tr.id_tax AND t.active = 1
GROUP BY tr.id_tax_rules_group
) tax_data ON tax_data.id_tax_rules_group = product_shop.id_tax_rules_group
LEFT JOIN %scategory_lang cl
ON cl.id_category = product_shop.id_category_default
AND cl.id_lang = pl.id_lang
AND cl.id_shop = pl.id_shop
WHERE cp.id_cart = ?
AND product_shop.active = 1
ORDER BY cp.date_add ASC, cp.id_product ASC, cp.id_product_attribute ASC
`, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix)
var items []Item
if err := s.db.WithContext(ctx).Raw(strings.TrimSpace(query), languageID, shopID, currencyID, cartID).Scan(&items).Error; err != nil {
return nil, err
}
page := &Page{ID: cartID, Items: items}
if err := s.loadItemAttributes(ctx, languageID, &page.Items); err != nil {
return nil, err
}
for _, item := range items {
page.TotalItems += item.Quantity
page.Subtotal += item.LineTotal
page.SubtotalTaxIncl += item.LineTotalTaxIncl
}
return page, nil
}
func (s *Service) loadItemAttributes(ctx context.Context, languageID int64, items *[]Item) error {
if items == nil || len(*items) == 0 {
return nil
}
attributeIDs := make([]int64, 0, len(*items))
indexByAttributeID := make(map[int64][]int)
for i, item := range *items {
if item.ProductAttributeID == 0 {
continue
}
if _, exists := indexByAttributeID[item.ProductAttributeID]; !exists {
attributeIDs = append(attributeIDs, item.ProductAttributeID)
}
indexByAttributeID[item.ProductAttributeID] = append(indexByAttributeID[item.ProductAttributeID], i)
}
if len(attributeIDs) == 0 {
return nil
}
type attributeRow struct {
ProductAttributeID int64 `gorm:"column:id_product_attribute"`
GroupName string `gorm:"column:group_name"`
AttributeName string `gorm:"column:attribute_name"`
}
query := fmt.Sprintf(`
SELECT pac.id_product_attribute,
agl.public_name AS group_name,
al.name AS attribute_name
FROM %sproduct_attribute_combination pac
JOIN %sattribute a
ON a.id_attribute = pac.id_attribute
JOIN %sattribute_lang al
ON al.id_attribute = a.id_attribute
AND al.id_lang = ?
JOIN %sattribute_group ag
ON ag.id_attribute_group = a.id_attribute_group
JOIN %sattribute_group_lang agl
ON agl.id_attribute_group = ag.id_attribute_group
AND agl.id_lang = ?
WHERE pac.id_product_attribute IN ?
ORDER BY pac.id_product_attribute ASC, ag.position ASC, a.position ASC, al.name ASC
`, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix)
rows := make([]attributeRow, 0)
if err := s.db.WithContext(ctx).Raw(strings.TrimSpace(query), languageID, languageID, attributeIDs).Scan(&rows).Error; err != nil {
return err
}
for _, row := range rows {
indices := indexByAttributeID[row.ProductAttributeID]
for _, idx := range indices {
(*items)[idx].Attributes = append((*items)[idx].Attributes, ItemAttribute{
Group: strings.TrimSpace(row.GroupName),
Value: strings.TrimSpace(row.AttributeName),
})
}
}
return nil
}
func (s *Service) AddProduct(ctx context.Context, input MutationInput) (*MutationResult, error) {
return s.mutateProduct(ctx, mutationAdd, input)
}
func (s *Service) UpdateProduct(ctx context.Context, input MutationInput) (*MutationResult, error) {
return s.mutateProduct(ctx, mutationSet, input)
}
func (s *Service) DeleteProduct(ctx context.Context, input MutationInput) (*MutationResult, error) {
return s.mutateProduct(ctx, mutationDelete, input)
}
type mutationMode string
const (
mutationAdd mutationMode = "add"
mutationSet mutationMode = "set"
mutationDelete mutationMode = "delete"
)
type productLine struct {
CartID int64 `gorm:"column:id_cart"`
ProductID int64 `gorm:"column:id_product"`
ProductAttributeID int64 `gorm:"column:id_product_attribute"`
CustomizationID int64 `gorm:"column:id_customization"`
AddressDeliveryID int64 `gorm:"column:id_address_delivery"`
Quantity int64 `gorm:"column:quantity"`
}
type cartContext struct {
ID int64
ShopID int64 `gorm:"column:id_shop"`
ShopGroupID int64 `gorm:"column:id_shop_group"`
CustomerID int64 `gorm:"column:id_customer"`
GuestID int64 `gorm:"column:id_guest"`
LanguageID int64 `gorm:"column:id_lang"`
CurrencyID int64 `gorm:"column:id_currency"`
AddressDeliveryID int64 `gorm:"column:id_address_delivery"`
AddressInvoiceID int64 `gorm:"column:id_address_invoice"`
SecureKey string
}
func (s *Service) mutateProduct(ctx context.Context, mode mutationMode, input MutationInput) (*MutationResult, error) {
if s == nil || s.db == nil {
return nil, errors.New("prestashop cart service is not initialized")
}
if input.ProductID == 0 {
return nil, errors.New("product id is required")
}
if mode != mutationDelete && input.Quantity <= 0 {
return nil, errors.New("quantity must be positive")
}
if input.ShopID == 0 {
input.ShopID = 1
}
if input.LanguageID == 0 {
input.LanguageID = 1
}
if input.CurrencyID == 0 {
input.CurrencyID = 1
}
var result MutationResult
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
cart, created, err := s.ensureCart(tx, input)
if err != nil {
return err
}
result.CartID = cart.ID
result.CreatedCart = created
attributeID, err := s.resolveProductAttributeID(tx, input.ProductID, input.ProductAttributeID, cart.ShopID)
if err != nil {
return err
}
input.ProductAttributeID = attributeID
if err := s.ensureProductExists(tx, input.ProductID, cart.ShopID); err != nil {
return err
}
line, err := s.loadProductLine(tx, cart.ID, input.ProductID, input.ProductAttributeID, input.CustomizationID)
if err != nil {
return err
}
switch mode {
case mutationDelete:
if err := s.deleteProductLine(tx, cart.ID, input.ProductID, input.ProductAttributeID, input.CustomizationID); err != nil {
return err
}
result.LineQuantity = 0
case mutationAdd:
addressID := cart.AddressDeliveryID
if line != nil && line.AddressDeliveryID != 0 {
addressID = line.AddressDeliveryID
}
newQty := input.Quantity
if line != nil {
newQty += line.Quantity
if err := s.updateProductLineQuantity(tx, line, newQty); err != nil {
return err
}
} else {
if err := s.insertProductLine(tx, cart, input.ProductID, input.ProductAttributeID, input.CustomizationID, addressID, newQty); err != nil {
return err
}
}
result.LineQuantity = newQty
case mutationSet:
if input.Quantity == 0 {
if err := s.deleteProductLine(tx, cart.ID, input.ProductID, input.ProductAttributeID, input.CustomizationID); err != nil {
return err
}
result.LineQuantity = 0
} else if line != nil {
if err := s.updateProductLineQuantity(tx, line, input.Quantity); err != nil {
return err
}
result.LineQuantity = input.Quantity
} else {
if err := s.insertProductLine(tx, cart, input.ProductID, input.ProductAttributeID, input.CustomizationID, cart.AddressDeliveryID, input.Quantity); err != nil {
return err
}
result.LineQuantity = input.Quantity
}
default:
return fmt.Errorf("unsupported cart mutation %q", mode)
}
if err := s.touchCart(tx, cart.ID); err != nil {
return err
}
summary, err := s.summaryByIDWithDB(tx, cart.ID)
if err != nil {
return err
}
result.TotalItems = summary.TotalItems
return nil
})
if err != nil {
return nil, err
}
return &result, nil
}
func (s *Service) ensureCart(tx *gorm.DB, input MutationInput) (*cartContext, bool, error) {
if input.CartID != 0 {
cart, err := s.loadCart(tx, input.CartID)
if err == nil {
return cart, false, nil
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, false, err
}
}
ctx := cartContext{
ShopID: input.ShopID,
CustomerID: input.CustomerID,
GuestID: input.GuestID,
LanguageID: input.LanguageID,
CurrencyID: input.CurrencyID,
}
shopGroupID, err := s.loadShopGroupID(tx, ctx.ShopID)
if err != nil {
return nil, false, err
}
ctx.ShopGroupID = shopGroupID
if ctx.CustomerID != 0 {
ctx.AddressDeliveryID, ctx.AddressInvoiceID, err = s.loadCustomerAddressIDs(tx, ctx.CustomerID)
if err != nil {
return nil, false, err
}
ctx.SecureKey, err = s.loadCustomerSecureKey(tx, ctx.CustomerID)
if err != nil {
return nil, false, err
}
}
cartID, err := s.insertCart(tx, &ctx)
if err != nil {
return nil, false, err
}
ctx.ID = cartID
return &ctx, true, nil
}
func (s *Service) loadCart(tx *gorm.DB, cartID int64) (*cartContext, error) {
var cart cartContext
query := fmt.Sprintf(`
SELECT id_cart AS id,
COALESCE(id_shop, 0) AS id_shop,
COALESCE(id_shop_group, 0) AS id_shop_group,
COALESCE(id_customer, 0) AS id_customer,
COALESCE(id_guest, 0) AS id_guest,
COALESCE(id_lang, 0) AS id_lang,
COALESCE(id_currency, 0) AS id_currency,
COALESCE(id_address_delivery, 0) AS id_address_delivery,
COALESCE(id_address_invoice, 0) AS id_address_invoice,
COALESCE(secure_key, '') AS secure_key
FROM %scart
WHERE id_cart = ?
LIMIT 1
`, s.prefix)
err := tx.Raw(strings.TrimSpace(query), cartID).Scan(&cart).Error
if err != nil {
return nil, err
}
if cart.ID == 0 {
return nil, gorm.ErrRecordNotFound
}
return &cart, nil
}
func (s *Service) loadShopGroupID(tx *gorm.DB, shopID int64) (int64, error) {
var row struct {
ShopGroupID int64 `gorm:"column:id_shop_group"`
}
query := fmt.Sprintf("SELECT id_shop_group FROM %sshop WHERE id_shop = ? LIMIT 1", s.prefix)
if err := tx.Raw(query, shopID).Scan(&row).Error; err != nil {
return 0, err
}
if row.ShopGroupID == 0 {
return 0, fmt.Errorf("prestashop shop %d not found", shopID)
}
return row.ShopGroupID, nil
}
func (s *Service) loadCustomerAddressIDs(tx *gorm.DB, customerID int64) (int64, int64, error) {
var row struct {
ID int64 `gorm:"column:id_address"`
}
query := fmt.Sprintf(`
SELECT id_address
FROM %saddress
WHERE id_customer = ?
AND deleted = 0
ORDER BY id_address ASC
LIMIT 1
`, s.prefix)
if err := tx.Raw(strings.TrimSpace(query), customerID).Scan(&row).Error; err != nil {
return 0, 0, err
}
return row.ID, row.ID, nil
}
func (s *Service) loadCustomerSecureKey(tx *gorm.DB, customerID int64) (string, error) {
var row struct {
SecureKey string `gorm:"column:secure_key"`
}
query := fmt.Sprintf("SELECT COALESCE(secure_key, '') AS secure_key FROM %scustomer WHERE id_customer = ? LIMIT 1", s.prefix)
if err := tx.Raw(query, customerID).Scan(&row).Error; err != nil {
return "", err
}
return row.SecureKey, nil
}
func (s *Service) insertCart(tx *gorm.DB, cart *cartContext) (int64, error) {
available, err := s.tableColumns(tx, s.prefix+"cart")
if err != nil {
return 0, err
}
now := time.Now().UTC().Format("2006-01-02 15:04:05")
columns := make([]string, 0, 16)
values := make([]any, 0, 16)
add := func(name string, value any) {
if available[name] {
columns = append(columns, name)
values = append(values, value)
}
}
add("id_shop_group", cart.ShopGroupID)
add("id_shop", cart.ShopID)
add("id_address_delivery", cart.AddressDeliveryID)
add("id_address_invoice", cart.AddressInvoiceID)
add("id_carrier", 0)
add("id_currency", cart.CurrencyID)
add("id_customer", cart.CustomerID)
add("id_guest", cart.GuestID)
add("id_lang", cart.LanguageID)
add("recyclable", 0)
add("gift", 0)
add("gift_message", "")
add("mobile_theme", 0)
add("delivery_option", "")
add("secure_key", cart.SecureKey)
add("allow_seperated_package", 0)
add("date_add", now)
add("date_upd", now)
if err := tx.Exec(insertQuery(s.prefix+"cart", columns), values...).Error; err != nil {
return 0, err
}
var row struct {
ID int64 `gorm:"column:id"`
}
if err := tx.Raw("SELECT LAST_INSERT_ID() AS id").Scan(&row).Error; err != nil {
return 0, err
}
if row.ID == 0 {
return 0, errors.New("cart insert did not return an id")
}
return row.ID, nil
}
func (s *Service) resolveProductAttributeID(tx *gorm.DB, productID, productAttributeID, shopID int64) (int64, error) {
if productAttributeID != 0 {
var row struct {
ID int64 `gorm:"column:id_product_attribute"`
}
query := fmt.Sprintf(`
SELECT pa.id_product_attribute
FROM %sproduct_attribute pa
WHERE pa.id_product_attribute = ?
AND pa.id_product = ?
LIMIT 1
`, s.prefix)
if err := tx.Raw(strings.TrimSpace(query), productAttributeID, productID).Scan(&row).Error; err != nil {
return 0, err
}
if row.ID == 0 {
return 0, fmt.Errorf("product attribute %d does not belong to product %d", productAttributeID, productID)
}
return row.ID, nil
}
var row struct {
ID int64 `gorm:"column:id_product_attribute"`
}
query := fmt.Sprintf(`
SELECT pa.id_product_attribute
FROM %sproduct_attribute pa
LEFT JOIN %sproduct_attribute_shop pas
ON pas.id_product_attribute = pa.id_product_attribute
AND pas.id_shop = ?
WHERE pa.id_product = ?
ORDER BY CASE WHEN COALESCE(pas.default_on, 0) = 1 THEN 0 ELSE 1 END,
pa.id_product_attribute ASC
LIMIT 1
`, s.prefix, s.prefix)
if err := tx.Raw(strings.TrimSpace(query), shopID, productID).Scan(&row).Error; err != nil {
return 0, err
}
return row.ID, nil
}
func (s *Service) ensureProductExists(tx *gorm.DB, productID, shopID int64) error {
var row struct {
ID int64 `gorm:"column:id_product"`
}
query := fmt.Sprintf(`
SELECT p.id_product
FROM %sproduct p
JOIN %sproduct_shop ps
ON ps.id_product = p.id_product
AND ps.id_shop = ?
WHERE p.id_product = ?
LIMIT 1
`, s.prefix, s.prefix)
if err := tx.Raw(strings.TrimSpace(query), shopID, productID).Scan(&row).Error; err != nil {
return err
}
if row.ID == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
func (s *Service) loadProductLine(tx *gorm.DB, cartID, productID, productAttributeID, customizationID int64) (*productLine, error) {
var line productLine
query := fmt.Sprintf(`
SELECT id_cart,
id_product,
COALESCE(id_product_attribute, 0) AS id_product_attribute,
COALESCE(id_customization, 0) AS id_customization,
COALESCE(id_address_delivery, 0) AS id_address_delivery,
quantity
FROM %scart_product
WHERE id_cart = ?
AND id_product = ?
AND COALESCE(id_product_attribute, 0) = ?
AND COALESCE(id_customization, 0) = ?
LIMIT 1
`, s.prefix)
if err := tx.Raw(strings.TrimSpace(query), cartID, productID, productAttributeID, customizationID).Scan(&line).Error; err != nil {
return nil, err
}
if line.CartID == 0 {
return nil, nil
}
return &line, nil
}
func (s *Service) insertProductLine(tx *gorm.DB, cart *cartContext, productID, productAttributeID, customizationID, addressDeliveryID, quantity int64) error {
available, err := s.tableColumns(tx, s.prefix+"cart_product")
if err != nil {
return err
}
now := time.Now().UTC().Format("2006-01-02 15:04:05")
columns := make([]string, 0, 8)
values := make([]any, 0, 8)
add := func(name string, value any) {
if available[name] {
columns = append(columns, name)
values = append(values, value)
}
}
add("id_product", productID)
add("id_product_attribute", productAttributeID)
add("id_cart", cart.ID)
add("id_address_delivery", addressDeliveryID)
add("id_shop", cart.ShopID)
add("quantity", quantity)
add("date_add", now)
add("id_customization", customizationID)
return tx.Exec(insertQuery(s.prefix+"cart_product", columns), values...).Error
}
func (s *Service) updateProductLineQuantity(tx *gorm.DB, line *productLine, quantity int64) error {
query := fmt.Sprintf(`
UPDATE %scart_product
SET quantity = ?
WHERE id_cart = ?
AND id_product = ?
AND COALESCE(id_product_attribute, 0) = ?
AND COALESCE(id_customization, 0) = ?
LIMIT 1
`, s.prefix)
return tx.Exec(strings.TrimSpace(query), quantity, line.CartID, line.ProductID, line.ProductAttributeID, line.CustomizationID).Error
}
func (s *Service) deleteProductLine(tx *gorm.DB, cartID, productID, productAttributeID, customizationID int64) error {
query := fmt.Sprintf(`
DELETE FROM %scart_product
WHERE id_cart = ?
AND id_product = ?
AND COALESCE(id_product_attribute, 0) = ?
AND COALESCE(id_customization, 0) = ?
`, s.prefix)
return tx.Exec(strings.TrimSpace(query), cartID, productID, productAttributeID, customizationID).Error
}
func (s *Service) touchCart(tx *gorm.DB, cartID int64) error {
query := fmt.Sprintf("UPDATE %scart SET date_upd = ? WHERE id_cart = ?", s.prefix)
return tx.Exec(query, time.Now().UTC().Format("2006-01-02 15:04:05"), cartID).Error
}
func (s *Service) summaryByIDWithDB(tx *gorm.DB, cartID int64) (*Summary, error) {
var summary Summary
query := fmt.Sprintf("SELECT id_cart AS id, COALESCE(SUM(quantity), 0) AS total_items FROM %scart_product WHERE id_cart = ? GROUP BY id_cart", s.prefix)
result := tx.Raw(query, cartID).Scan(&summary)
if result.Error != nil {
return nil, result.Error
}
if result.RowsAffected == 0 {
return &Summary{ID: cartID}, nil
}
return &summary, nil
}
func (s *Service) tableColumns(tx *gorm.DB, tableName string) (map[string]bool, error) {
type row struct {
ColumnName string `gorm:"column:COLUMN_NAME"`
}
var rows []row
query := `
SELECT COLUMN_NAME
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = ?
`
if err := tx.Raw(query, tableName).Scan(&rows).Error; err != nil {
return nil, err
}
columns := make(map[string]bool, len(rows))
for _, row := range rows {
columns[row.ColumnName] = true
}
return columns, nil
}
func insertQuery(tableName string, columns []string) string {
if len(columns) == 0 {
return fmt.Sprintf("INSERT INTO %s () VALUES ()", tableName)
}
return fmt.Sprintf(
"INSERT INTO %s (%s) VALUES (%s)",
tableName,
strings.Join(columns, ", "),
placeholders(len(columns)),
)
}
func placeholders(n int) string {
parts := make([]string, n)
for i := range parts {
parts[i] = "?"
}
return strings.Join(parts, ", ")
}
func formatInt64(value int64) string {
if value == 0 {
return ""
}
return strconv.FormatInt(value, 10)
}
+478 -24
View File
@@ -15,6 +15,7 @@ type ProductPageRequest struct {
Slug string
LanguageID int64
ShopID int64
CurrencyID int64
}
type CategoryPageRequest struct {
@@ -22,6 +23,9 @@ type CategoryPageRequest struct {
Slug string
LanguageID int64
ShopID int64
CurrencyID int64
Page int
PerPage int
}
type ProductPageData struct {
@@ -31,10 +35,55 @@ type ProductPageData struct {
ShortDescription string
Description string
Price float64
PriceTaxIncl float64 `gorm:"column:price_tax_incl"`
TaxRate float64 `gorm:"column:tax_rate"`
CurrencyID int64 `gorm:"column:currency_id"`
CurrencyCode string `gorm:"column:currency_code"`
CurrencySign string `gorm:"column:currency_sign"`
ConversionRate float64 `gorm:"column:conversion_rate"`
CoverImageID sql.NullInt64
ImageURL string `gorm:"-"`
GalleryImages []ProductImage `gorm:"-"`
CategoryID int64
CategorySlug string
CategoryName string
Features []ProductFeature `gorm:"-"`
Accessories []CategoryProductCard `gorm:"-"`
Combinations []ProductCombination `gorm:"-"`
DefaultAttribute int64 `gorm:"-"`
}
type ProductImage struct {
ID int64 `gorm:"column:id_image"`
Cover bool `gorm:"column:cover"`
Position int `gorm:"column:position"`
URL string `gorm:"-"`
ThumbURL string `gorm:"-"`
}
type ProductFeature struct {
ID int64 `gorm:"column:id_feature"`
Name string `gorm:"column:name"`
Value string `gorm:"column:value"`
}
type ProductCombination struct {
ID int64 `gorm:"column:id_product_attribute"`
Price float64 `gorm:"column:price"`
PriceTaxIncl float64 `gorm:"column:price_tax_incl"`
DefaultOn bool `gorm:"column:default_on"`
ImageID sql.NullInt64 `gorm:"-"`
ImageURL string `gorm:"-"`
ThumbURL string `gorm:"-"`
Attributes []ProductCombinationAttribute `gorm:"-"`
}
type ProductCombinationAttribute struct {
Group string
PublicName string
Value string
GroupType string
Color string
}
type CategoryPageData struct {
@@ -43,16 +92,33 @@ type CategoryPageData struct {
Slug string
Description string
Products []CategoryProductCard `gorm:"-"`
Pagination CategoryPagination `gorm:"-"`
}
type CategoryPagination struct {
Page int
PerPage int
TotalItems int64
TotalPages int
}
type CategoryProductCard struct {
ID int64
Name string
Slug string
URL string `gorm:"-"`
Price float64
Description string
EAN13 string
ID int64
Name string
Slug string
CategorySlug string `gorm:"column:category_slug"`
URL string `gorm:"-"`
ImageURL string `gorm:"-"`
Price float64
PriceTaxIncl float64 `gorm:"column:price_tax_incl"`
TaxRate float64 `gorm:"column:tax_rate"`
CurrencyID int64 `gorm:"column:currency_id"`
CurrencyCode string `gorm:"column:currency_code"`
CurrencySign string `gorm:"column:currency_sign"`
ConversionRate float64 `gorm:"column:conversion_rate"`
CoverImageID sql.NullInt64 `gorm:"column:cover_image_id"`
ShortDescription string `gorm:"column:short_description"`
EAN13 string
}
type MenuItem struct {
@@ -92,54 +158,85 @@ func NewService(db *gorm.DB, prefix string) *Service {
func (s *Service) GetProductPage(ctx context.Context, req ProductPageRequest) (*ProductPageData, error) {
var product ProductPageData
if req.CurrencyID == 0 {
req.CurrencyID = 1
}
queryByID := fmt.Sprintf(`
SELECT p.id_product AS id,
pl.name AS name,
pl.link_rewrite AS slug,
pl.description_short AS short_description,
pl.description AS description,
ps.price AS price,
(ps.price * curr.conversion_rate) AS price,
((ps.price * curr.conversion_rate) * (1 + COALESCE(tax_data.tax_rate, 0) / 100)) AS price_tax_incl,
COALESCE(tax_data.tax_rate, 0) AS tax_rate,
curr.id_currency AS currency_id,
curr.iso_code AS currency_code,
curr.sign AS currency_sign,
curr.conversion_rate AS conversion_rate,
i.id_image AS cover_image_id,
p.id_category_default AS category_id,
cl.link_rewrite AS category_slug,
cl.name AS category_name
FROM %sproduct p
JOIN %sproduct_lang pl ON pl.id_product = p.id_product
JOIN %sproduct_shop ps ON ps.id_product = p.id_product
JOIN %sproduct_shop ps ON ps.id_product = p.id_product AND ps.id_shop = ?
JOIN %scurrency curr ON curr.id_currency = ? AND curr.deleted = 0
LEFT JOIN (
SELECT tr.id_tax_rules_group,
SUM(t.rate) AS tax_rate
FROM %stax_rule tr
JOIN %stax t ON t.id_tax = tr.id_tax AND t.active = 1
GROUP BY tr.id_tax_rules_group
) tax_data ON tax_data.id_tax_rules_group = ps.id_tax_rules_group
LEFT JOIN %scategory_lang cl ON cl.id_category = p.id_category_default AND cl.id_lang = pl.id_lang
LEFT JOIN %simage i ON i.id_product = p.id_product AND i.cover = 1
WHERE p.id_product = ?
AND ps.active = 1
AND pl.id_lang = ?
AND ps.id_shop = ?
LIMIT 1
`, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix)
`, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix)
queryBySlug := fmt.Sprintf(`
SELECT p.id_product AS id,
pl.name AS name,
pl.link_rewrite AS slug,
pl.description_short AS short_description,
pl.description AS description,
ps.price AS price,
(ps.price * curr.conversion_rate) AS price,
((ps.price * curr.conversion_rate) * (1 + COALESCE(tax_data.tax_rate, 0) / 100)) AS price_tax_incl,
COALESCE(tax_data.tax_rate, 0) AS tax_rate,
curr.id_currency AS currency_id,
curr.iso_code AS currency_code,
curr.sign AS currency_sign,
curr.conversion_rate AS conversion_rate,
i.id_image AS cover_image_id,
p.id_category_default AS category_id,
cl.link_rewrite AS category_slug,
cl.name AS category_name
FROM %sproduct p
JOIN %sproduct_lang pl ON pl.id_product = p.id_product
JOIN %sproduct_shop ps ON ps.id_product = p.id_product
JOIN %sproduct_shop ps ON ps.id_product = p.id_product AND ps.id_shop = ?
JOIN %scurrency curr ON curr.id_currency = ? AND curr.deleted = 0
LEFT JOIN (
SELECT tr.id_tax_rules_group,
SUM(t.rate) AS tax_rate
FROM %stax_rule tr
JOIN %stax t ON t.id_tax = tr.id_tax AND t.active = 1
GROUP BY tr.id_tax_rules_group
) tax_data ON tax_data.id_tax_rules_group = ps.id_tax_rules_group
LEFT JOIN %scategory_lang cl ON cl.id_category = p.id_category_default AND cl.id_lang = pl.id_lang
LEFT JOIN %simage i ON i.id_product = p.id_product AND i.cover = 1
WHERE pl.link_rewrite = ?
AND ps.active = 1
AND pl.id_lang = ?
AND ps.id_shop = ?
LIMIT 1
`, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix)
`, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix)
var result *gorm.DB
if req.ID != 0 {
result = s.db.WithContext(ctx).Raw(strings.TrimSpace(queryByID), req.ID, req.LanguageID, req.ShopID).Scan(&product)
result = s.db.WithContext(ctx).Raw(strings.TrimSpace(queryByID), req.ShopID, req.CurrencyID, req.ID, req.LanguageID).Scan(&product)
} else {
result = s.db.WithContext(ctx).Raw(strings.TrimSpace(queryBySlug), req.Slug, req.LanguageID, req.ShopID).Scan(&product)
result = s.db.WithContext(ctx).Raw(strings.TrimSpace(queryBySlug), req.ShopID, req.CurrencyID, req.Slug, req.LanguageID).Scan(&product)
}
if result.Error != nil {
return nil, result.Error
@@ -147,12 +244,316 @@ LIMIT 1
if result.RowsAffected == 0 {
return nil, gorm.ErrRecordNotFound
}
features, err := s.loadProductFeatures(ctx, product.ID, req.LanguageID, req.ShopID)
if err != nil {
return nil, err
}
product.Features = features
images, err := s.loadProductImages(ctx, product.ID)
if err != nil {
return nil, err
}
product.GalleryImages = images
accessories, err := s.loadProductAccessories(ctx, product.ID, req.LanguageID, req.ShopID, req.CurrencyID)
if err != nil {
return nil, err
}
product.Accessories = accessories
combinations, err := s.loadProductCombinations(ctx, product.ID, req.LanguageID, req.ShopID, req.CurrencyID)
if err != nil {
return nil, err
}
product.Combinations = combinations
for _, combination := range combinations {
if combination.DefaultOn {
product.DefaultAttribute = combination.ID
break
}
}
if product.DefaultAttribute == 0 && len(combinations) > 0 {
product.DefaultAttribute = combinations[0].ID
}
return &product, nil
}
func (s *Service) loadProductImages(ctx context.Context, productID int64) ([]ProductImage, error) {
if productID == 0 {
return nil, nil
}
query := fmt.Sprintf(`
SELECT i.id_image,
CASE WHEN COALESCE(i.cover, 0) = 1 THEN 1 ELSE 0 END AS cover,
COALESCE(i.position, 0) AS position
FROM %simage i
WHERE i.id_product = ?
ORDER BY CASE WHEN COALESCE(i.cover, 0) = 1 THEN 0 ELSE 1 END,
COALESCE(i.position, 0) ASC,
i.id_image ASC
`, s.prefix)
images := make([]ProductImage, 0)
if err := s.db.WithContext(ctx).Raw(strings.TrimSpace(query), productID).Scan(&images).Error; err != nil {
return nil, err
}
return images, nil
}
func (s *Service) loadProductAccessories(ctx context.Context, productID, languageID, shopID, currencyID int64) ([]CategoryProductCard, error) {
if productID == 0 {
return nil, nil
}
query := fmt.Sprintf(`
SELECT p.id_product AS id,
pl.name AS name,
pl.link_rewrite AS slug,
p.ean13 AS ean13,
cl.link_rewrite AS category_slug,
(ps.price * curr.conversion_rate) AS price,
((ps.price * curr.conversion_rate) * (1 + COALESCE(tax_data.tax_rate, 0) / 100)) AS price_tax_incl,
COALESCE(tax_data.tax_rate, 0) AS tax_rate,
curr.id_currency AS currency_id,
curr.iso_code AS currency_code,
curr.sign AS currency_sign,
curr.conversion_rate AS conversion_rate,
i.id_image AS cover_image_id,
pl.description_short AS short_description
FROM %saccessory a
JOIN %sproduct p ON p.id_product = a.id_product_2
JOIN %sproduct_lang pl ON pl.id_product = p.id_product
JOIN %sproduct_shop ps ON ps.id_product = p.id_product AND ps.id_shop = ?
JOIN %scurrency curr ON curr.id_currency = ? AND curr.deleted = 0
LEFT JOIN %simage i ON i.id_product = p.id_product AND i.cover = 1
LEFT JOIN %scategory_lang cl ON cl.id_category = p.id_category_default AND cl.id_lang = pl.id_lang
LEFT JOIN (
SELECT tr.id_tax_rules_group,
SUM(t.rate) AS tax_rate
FROM %stax_rule tr
JOIN %stax t ON t.id_tax = tr.id_tax AND t.active = 1
GROUP BY tr.id_tax_rules_group
) tax_data ON tax_data.id_tax_rules_group = ps.id_tax_rules_group
WHERE a.id_product_1 = ?
AND ps.active = 1
AND pl.id_lang = ?
ORDER BY p.id_product ASC
`, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix)
products := make([]CategoryProductCard, 0)
if err := s.db.WithContext(ctx).Raw(strings.TrimSpace(query), shopID, currencyID, productID, languageID).Scan(&products).Error; err != nil {
return nil, err
}
return products, nil
}
func (s *Service) loadProductFeatures(ctx context.Context, productID, languageID, shopID int64) ([]ProductFeature, error) {
if productID == 0 {
return nil, nil
}
query := fmt.Sprintf(`
SELECT pf.id_feature,
fl.name AS name,
fvl.value AS value
FROM %sfeature_product pf
LEFT JOIN %sfeature_lang fl
ON fl.id_feature = pf.id_feature
AND fl.id_lang = ?
LEFT JOIN %sfeature_value_lang fvl
ON fvl.id_feature_value = pf.id_feature_value
AND fvl.id_lang = ?
LEFT JOIN %sfeature f
ON f.id_feature = pf.id_feature
LEFT JOIN %sfeature_shop fs
ON fs.id_feature = f.id_feature
AND fs.id_shop = ?
WHERE pf.id_product = ?
ORDER BY f.position ASC
`, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix)
features := make([]ProductFeature, 0)
if err := s.db.WithContext(ctx).Raw(
strings.TrimSpace(query),
languageID,
languageID,
shopID,
productID,
).Scan(&features).Error; err != nil {
return nil, err
}
return features, nil
}
func (s *Service) loadProductCombinations(ctx context.Context, productID, languageID, shopID, currencyID int64) ([]ProductCombination, error) {
if productID == 0 {
return nil, nil
}
type combinationRow struct {
ID int64 `gorm:"column:id_product_attribute"`
Price float64 `gorm:"column:price"`
PriceTaxIncl float64 `gorm:"column:price_tax_incl"`
DefaultOn bool `gorm:"column:default_on"`
GroupName string `gorm:"column:group_name"`
PublicName string `gorm:"column:public_group_name"`
Attribute string `gorm:"column:attribute_name"`
GroupType string `gorm:"column:group_type"`
Color string `gorm:"column:attribute_color"`
}
query := fmt.Sprintf(`
SELECT pa.id_product_attribute,
((ps.price + COALESCE(pas.price, 0)) * curr.conversion_rate) AS price,
(((ps.price + COALESCE(pas.price, 0)) * curr.conversion_rate) * (1 + COALESCE(tax_data.tax_rate, 0) / 100)) AS price_tax_incl,
CASE WHEN COALESCE(pas.default_on, pa.default_on, 0) = 1 THEN 1 ELSE 0 END AS default_on,
agl.name AS group_name,
agl.public_name AS public_group_name,
al.name AS attribute_name,
ag.group_type AS group_type,
a.color AS attribute_color
FROM %sproduct_attribute pa
JOIN %sproduct_shop ps
ON ps.id_product = pa.id_product
AND ps.id_shop = ?
JOIN %scurrency curr
ON curr.id_currency = ?
AND curr.deleted = 0
LEFT JOIN (
SELECT tr.id_tax_rules_group,
SUM(t.rate) AS tax_rate
FROM %stax_rule tr
JOIN %stax t ON t.id_tax = tr.id_tax AND t.active = 1
GROUP BY tr.id_tax_rules_group
) tax_data
ON tax_data.id_tax_rules_group = ps.id_tax_rules_group
LEFT JOIN %sproduct_attribute_shop pas
ON pas.id_product_attribute = pa.id_product_attribute
AND pas.id_shop = ?
JOIN %sproduct_attribute_combination pac
ON pac.id_product_attribute = pa.id_product_attribute
JOIN %sattribute a
ON a.id_attribute = pac.id_attribute
JOIN %sattribute_lang al
ON al.id_attribute = a.id_attribute
AND al.id_lang = ?
JOIN %sattribute_group ag
ON ag.id_attribute_group = a.id_attribute_group
JOIN %sattribute_group_lang agl
ON agl.id_attribute_group = ag.id_attribute_group
AND agl.id_lang = ?
WHERE pa.id_product = ?
ORDER BY CASE WHEN COALESCE(pas.default_on, pa.default_on, 0) = 1 THEN 0 ELSE 1 END,
pa.id_product_attribute ASC,
ag.position ASC,
a.position ASC,
al.name ASC
`, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix)
rows := make([]combinationRow, 0)
if err := s.db.WithContext(ctx).Raw(
strings.TrimSpace(query),
shopID,
currencyID,
shopID,
languageID,
languageID,
productID,
).Scan(&rows).Error; err != nil {
return nil, err
}
combinations := make([]ProductCombination, 0)
indexByID := make(map[int64]int)
for _, row := range rows {
idx, exists := indexByID[row.ID]
if !exists {
combinations = append(combinations, ProductCombination{
ID: row.ID,
Price: row.Price,
PriceTaxIncl: row.PriceTaxIncl,
DefaultOn: row.DefaultOn,
})
idx = len(combinations) - 1
indexByID[row.ID] = idx
}
if strings.TrimSpace(row.GroupName) != "" || strings.TrimSpace(row.Attribute) != "" {
combinations[idx].Attributes = append(combinations[idx].Attributes, ProductCombinationAttribute{
Group: row.GroupName,
PublicName: row.PublicName,
Value: row.Attribute,
GroupType: row.GroupType,
Color: row.Color,
})
}
}
if err := s.loadCombinationImageIDs(ctx, &combinations); err != nil {
return nil, err
}
return combinations, nil
}
func (s *Service) loadCombinationImageIDs(ctx context.Context, combinations *[]ProductCombination) error {
if combinations == nil || len(*combinations) == 0 {
return nil
}
combinationIDs := make([]int64, 0, len(*combinations))
indexByID := make(map[int64]int, len(*combinations))
for i, combination := range *combinations {
if combination.ID == 0 {
continue
}
combinationIDs = append(combinationIDs, combination.ID)
indexByID[combination.ID] = i
}
if len(combinationIDs) == 0 {
return nil
}
type combinationImageRow struct {
ID int64 `gorm:"column:id_product_attribute"`
ImageID int64 `gorm:"column:id_image"`
}
query := fmt.Sprintf(`
SELECT pai.id_product_attribute,
MIN(pai.id_image) AS id_image
FROM %sproduct_attribute_image pai
WHERE pai.id_product_attribute IN ?
GROUP BY pai.id_product_attribute
`, s.prefix)
rows := make([]combinationImageRow, 0)
if err := s.db.WithContext(ctx).Raw(strings.TrimSpace(query), combinationIDs).Scan(&rows).Error; err != nil {
return err
}
for _, row := range rows {
idx, exists := indexByID[row.ID]
if !exists || row.ImageID == 0 {
continue
}
(*combinations)[idx].ImageID = sql.NullInt64{Int64: row.ImageID, Valid: true}
}
return nil
}
func (s *Service) GetCategoryPage(ctx context.Context, req CategoryPageRequest) (*CategoryPageData, error) {
var category CategoryPageData
if req.CurrencyID == 0 {
req.CurrencyID = 1
}
if req.Page <= 0 {
req.Page = 1
}
if req.PerPage <= 0 {
req.PerPage = 20
}
categoryQuery := fmt.Sprintf(`
SELECT c.id_category AS id,
cl.name AS name,
@@ -213,25 +614,67 @@ LIMIT 1
}
}
countQuery := fmt.Sprintf(`
SELECT COUNT(*) AS total_items
FROM %scategory_product cp
JOIN %sproduct p ON p.id_product = cp.id_product
JOIN %sproduct_lang pl ON pl.id_product = p.id_product
JOIN %sproduct_shop ps ON ps.id_product = p.id_product AND ps.id_shop = ?
WHERE cp.id_category = ?
AND ps.active = 1
AND pl.id_lang = ?
`, s.prefix, s.prefix, s.prefix, s.prefix)
var countRow struct {
TotalItems int64 `gorm:"column:total_items"`
}
if err := s.db.WithContext(ctx).Raw(strings.TrimSpace(countQuery), req.ShopID, category.ID, req.LanguageID).Scan(&countRow).Error; err != nil {
return nil, err
}
category.Pagination = CategoryPagination{
Page: req.Page,
PerPage: req.PerPage,
TotalItems: countRow.TotalItems,
TotalPages: totalPages(countRow.TotalItems, req.PerPage),
}
offset := (req.Page - 1) * req.PerPage
productQuery := fmt.Sprintf(`
SELECT p.id_product AS id,
pl.name AS name,
pl.link_rewrite AS slug,
p.ean13 AS ean13,
ps.price AS price,
pl.description_short AS description
(ps.price * curr.conversion_rate) AS price,
((ps.price * curr.conversion_rate) * (1 + COALESCE(tax_data.tax_rate, 0) / 100)) AS price_tax_incl,
COALESCE(tax_data.tax_rate, 0) AS tax_rate,
curr.id_currency AS currency_id,
curr.iso_code AS currency_code,
curr.sign AS currency_sign,
curr.conversion_rate AS conversion_rate,
i.id_image AS cover_image_id,
pl.description_short AS short_description
FROM %scategory_product cp
JOIN %sproduct p ON p.id_product = cp.id_product
JOIN %sproduct_lang pl ON pl.id_product = p.id_product
JOIN %sproduct_shop ps ON ps.id_product = p.id_product
JOIN %sproduct_shop ps ON ps.id_product = p.id_product AND ps.id_shop = ?
JOIN %scurrency curr ON curr.id_currency = ? AND curr.deleted = 0
LEFT JOIN %simage i ON i.id_product = p.id_product AND i.cover = 1
LEFT JOIN (
SELECT tr.id_tax_rules_group,
SUM(t.rate) AS tax_rate
FROM %stax_rule tr
JOIN %stax t ON t.id_tax = tr.id_tax AND t.active = 1
GROUP BY tr.id_tax_rules_group
) tax_data ON tax_data.id_tax_rules_group = ps.id_tax_rules_group
WHERE cp.id_category = ?
AND ps.active = 1
AND pl.id_lang = ?
AND ps.id_shop = ?
ORDER BY cp.position ASC, p.id_product ASC
LIMIT 48
`, s.prefix, s.prefix, s.prefix, s.prefix)
LIMIT ?
OFFSET ?
`, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix)
if err := s.db.WithContext(ctx).Raw(strings.TrimSpace(productQuery), category.ID, req.LanguageID, req.ShopID).Scan(&category.Products).Error; err != nil {
if err := s.db.WithContext(ctx).Raw(strings.TrimSpace(productQuery), req.ShopID, req.CurrencyID, category.ID, req.LanguageID, req.PerPage, offset).Scan(&category.Products).Error; err != nil {
return nil, err
}
@@ -266,6 +709,17 @@ func (s *Service) ResolveLanguageID(ctx context.Context, req *http.Request, fall
return row.ID
}
func totalPages(totalItems int64, perPage int) int {
if totalItems <= 0 || perPage <= 0 {
return 0
}
pages := int(totalItems / int64(perPage))
if totalItems%int64(perPage) != 0 {
pages++
}
return pages
}
func (s *Service) GetCategoryMenu(ctx context.Context, languageID int64, shopID int64) ([]MenuItem, error) {
rootCategoryID, err := s.rootCategoryID(ctx)
if err != nil {
+28
View File
@@ -129,6 +129,9 @@ func (r *CategoryRoute) Match(path string) (slug string, ok bool) {
}
func (r *CategoryRoute) MatchInfo(path string) (*CategoryMatch, bool) {
if hasExcludedStaticSegment(path) {
return nil, false
}
if r == nil || r.regex == nil {
return fallbackCategoryMatch(path)
}
@@ -262,6 +265,9 @@ func (r *ProductRoute) Match(path string) (slug string, ok bool) {
}
func (r *ProductRoute) MatchInfo(path string) (*ProductMatch, bool) {
if hasExcludedStaticSegment(path) {
return nil, false
}
if r == nil || r.regex == nil {
return fallbackProductMatch(path)
}
@@ -500,6 +506,9 @@ func fallbackProductMatch(path string) (*ProductMatch, bool) {
if path == "" {
return nil, false
}
if hasExcludedStaticSegment(path) {
return nil, false
}
if hasExcludedContentSegment(path) {
return nil, false
}
@@ -543,6 +552,9 @@ func fallbackCategoryMatch(path string) (*CategoryMatch, bool) {
if path == "" {
return nil, false
}
if hasExcludedStaticSegment(path) {
return nil, false
}
if hasExcludedContentSegment(path) {
return nil, false
}
@@ -601,3 +613,19 @@ func hasExcludedContentSegment(path string) bool {
}
return false
}
func hasExcludedStaticSegment(path string) bool {
path = strings.TrimSpace(path)
if path == "" {
return false
}
path = strings.Trim(path, "/")
if path == "" {
return false
}
first := path
if idx := strings.IndexByte(first, '/'); idx >= 0 {
first = first[:idx]
}
return strings.EqualFold(strings.TrimSpace(first), "img")
}
@@ -0,0 +1,25 @@
package routes
import "testing"
func TestCategoryRouteDoesNotOwnImagePath(t *testing.T) {
route, err := CompileCategoryRoute("/{id}-{rewrite}")
if err != nil {
t.Fatalf("compile category route: %v", err)
}
if match, ok := route.MatchInfo("/img/p/1/1/9/6/1/2/119612-large_default.webp"); ok || match != nil {
t.Fatalf("expected image path to bypass category route, got ok=%v match=%+v", ok, match)
}
}
func TestProductRouteDoesNotOwnImagePath(t *testing.T) {
route, err := CompileProductRoute("/{id}-{rewrite}")
if err != nil {
t.Fatalf("compile product route: %v", err)
}
if match, ok := route.MatchInfo("/img/p/1/1/9/6/1/2/119612-large_default.webp"); ok || match != nil {
t.Fatalf("expected image path to bypass product route, got ok=%v match=%+v", ok, match)
}
}