771 lines
22 KiB
Go
771 lines
22 KiB
Go
package cart
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type Summary struct {
|
|
ID int64
|
|
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
|
|
}
|
|
|
|
func NewService(db *gorm.DB, prefix string) *Service {
|
|
return &Service{db: db, prefix: prefix}
|
|
}
|
|
|
|
func (s *Service) SummaryByID(ctx context.Context, 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 := s.db.WithContext(ctx).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) 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)
|
|
}
|