Files
backend_fiesta/repositories/productRepository.go
Suriya fab06bb33e Bridge global catalogue DB to per-store product catalogue
Adds a separate CatalogueDB (pgvector) connection alongside the main
nearledb, plus a new catalogue module (repository/service/controller/
routes) to browse it by brand, category, and keyword, with brand
optional so the whole ~237-product catalogue can be browsed unfiltered.

Adds the actual bridge: importing a catalogue product snapshots it into
the tenant's own products table (keyed on brand+catalogueid, since a
catalogue row's bare id is only unique within its own brand table),
then links it via the existing productlocations upsert. Re-importing
tops up stock and refreshes price instead of duplicating. Also adds an
imported-refs endpoint so the frontend can badge already-imported items
without diffing full product lists, and wires the new AWS S3 image
store used to resolve catalogue product photos.

Bumps Go/Docker to 1.24 for the AWS SDK dependency this needs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 17:34:20 +05:30

818 lines
25 KiB
Go

package repositories
import (
"errors"
"fmt"
"strconv"
"strings"
"time"
"nearle/models"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type ProductRepository interface {
GetProductSubCategory(categoryID, tenantID int) ([]models.ProductSubCategory, error)
GetProductCount(tenantID, categoryID, subcategoryID int, approve string) ([]models.Productcount, error)
GetProductCategory() ([]models.ProductCategory, error)
GetProductVariants(tenantID, subcategoryID int) ([]models.Productvariant, error)
GetCatalougeProducts(tenantID, locationID, subcategoryID, pageno, pagesize int, keyword string) ([]models.Products, error)
GetProductStocks(tenantID, locationID string) ([]models.Productstocks, error)
CreateProductStock(stocks []models.Productstock) error
UpdateProductStatus(productIDs []int, status string) error
CreateProduct(product models.Products) error
UpdateProduct(product models.Products) error
DeleteProduct(productID int) error
GetStockStatement(tenantID, locationID, subcategoryID, pageno, pagesize int, keyword string) ([]models.Productstockstatement, error)
GetLocationProducts(tenantID, locationID, subcategoryID, pageno, pagesize int, keyword string) ([]models.Locationproducts, error)
GetLocationProductSummary(tenantID, locationID int) ([]models.ProductSummary, error)
FetchFilteredProducts(categoryID, subcategoryID, productID, applocationID, tenantID, locationID int, keyword, productStatus, approve string, pageno, pagesize int) ([]models.Tenantproducts, error)
GetProductByVariant(tenantid, variantid int) ([]models.Products, error)
GetSubcategories(categoryID int) ([]models.Subcategory, error)
GetProducts(params models.ProductFilter) ([]models.Products, error)
GetTenantInfo(tenantID, applocationID int) (map[string]interface{}, error)
UpdateProductLocation(input models.Productlocations) error
CreateProductLocation(input []models.Productlocations) error
CreateProductVariant(input models.Productvariant) error
DeleteProductLocation(tenantid, locationid, productid int) error
FindTenantProductByCatalogueRef(tenantid int, brand string, catalogueid int64) (*models.Products, error)
CreateProductReturningID(product models.Products) (int, error)
GetImportedCatalogueRefs(tenantid int, brand string) ([]models.ImportedCatalogueRef, error)
UpdateProductPricing(productid int, retailprice, productcost, taxpercent float64) error
}
type productRepository struct {
db *gorm.DB
}
func NewProductRepository(db *gorm.DB) ProductRepository {
return &productRepository{db: db}
}
func (r *productRepository) GetProductSubCategory(categoryID, tenantID int) ([]models.ProductSubCategory, error) {
var data []models.ProductSubCategory
var query strings.Builder
var args []interface{}
query.WriteString("SELECT * FROM productsubcategories WHERE 1=1")
if tenantID != 0 {
query.WriteString(" AND tenantid = ?")
args = append(args, tenantID)
}
if categoryID != 0 {
query.WriteString(" AND categoryid = ?")
args = append(args, categoryID)
}
if err := r.db.Raw(query.String(), args...).Scan(&data).Error; err != nil {
return nil, err
}
// print()
return data, nil
}
func (r *productRepository) GetProductCount(tenantid, categoryid, subcategory int, approve string) ([]models.Productcount, error) {
var data []models.Productcount
baseQuery := `
SELECT
COUNT(*) AS total,
SUM(CASE WHEN a.productstatus = 'available' THEN 1 ELSE 0 END) AS available,
SUM(CASE WHEN a.productstatus = 'outofstock' THEN 1 ELSE 0 END) AS outofstock
FROM products a
WHERE 1 = 1
`
var conditions []string
var params []interface{}
if tenantid != 0 {
conditions = append(conditions, "a.tenantid = ?")
params = append(params, tenantid)
}
if categoryid != 0 {
conditions = append(conditions, "a.categoryid = ?")
params = append(params, categoryid)
}
if subcategory != 0 {
conditions = append(conditions, "a.subcategoryid = ?")
params = append(params, subcategory)
}
if approve != "" {
conditions = append(conditions, "a.approve = ?")
params = append(params, approve)
}
if len(conditions) > 0 {
baseQuery += " AND " + strings.Join(conditions, " AND ")
}
if err := r.db.Raw(baseQuery, params...).Scan(&data).Error; err != nil {
return nil, err
}
print(baseQuery)
return data, nil
}
func (r *productRepository) GetProductCategory() ([]models.ProductCategory, error) {
var data []models.ProductCategory
q1 := `SELECT * FROM productcategories WHERE moduleid = 2 AND status = 'Active'`
r.db.Raw(q1).Scan(&data)
print(q1)
return data, nil
}
func (r *productRepository) GetProductVariants(tenantID int, subcategoryID int) ([]models.Productvariant, error) {
var data []models.Productvariant
var query string
var params []interface{}
query = `
SELECT a.*, b.categoryname
FROM productvariants a
JOIN app_category b ON a.categoryid = b.categoryid
WHERE a.tenantid = ?
`
params = append(params, tenantID)
if subcategoryID != 0 {
query += " AND a.subcategoryid = ?"
params = append(params, subcategoryID)
}
r.db.Raw(query, params...).Scan(&data)
//print(query)
return data, nil
}
func (r *productRepository) GetCatalougeProducts(tenantID, locationID, subcategoryID, pageno, pagesize int, keyword string) ([]models.Products, error) {
var data []models.Products
if pageno < 1 {
pageno = 1
}
if pagesize < 1 {
pagesize = 10
}
offset := (pageno - 1) * pagesize
params := []interface{}{locationID, tenantID}
// Base query
query := `
SELECT a.*
FROM products a
LEFT JOIN productlocations b
ON a.productid = b.productid
AND b.locationid = ?
AND b.tenantid = a.tenantid
WHERE a.approve = 1
AND a.tenantid = ?
AND b.productid IS NULL
`
// Optional filters
if subcategoryID != 0 {
query += " AND a.subcategoryid = ?"
params = append(params, subcategoryID)
}
if keyword != "" {
query += " AND LOWER(a.productname) LIKE ?"
params = append(params, "%"+strings.ToLower(keyword)+"%")
}
// Pagination
query += " ORDER BY a.productid DESC LIMIT " + strconv.Itoa(pagesize) + " OFFSET " + strconv.Itoa(offset)
// Debug logs
fmt.Println("Executing query:", query)
fmt.Println("Params:", params)
// Execute query
if err := r.db.Raw(query, params...).Scan(&data).Error; err != nil {
return nil, err
}
return data, nil
}
func (r *productRepository) GetProductStocks(tenantID, locationID string) ([]models.Productstocks, error) {
var stocks []models.Productstocks
var params []interface{}
var conditions []string
query := `
SELECT
a.productid, a.tenantid, MAX(a.stockdate) AS stockdate, a.locationid, a.stocktype, a.maxquantity, a.minquantity, a.status,
b.applocationid, b.categoryid, b.subcategoryid, b.catalogueid, b.addonid, b.discountid, b.pricingid,
b.productname, b.productimage, b.productdesc, b.productsku, b.brandid, b.productbrand, b.productunit,
b.unitvalue, b.toppicks, b.productcost, b.taxamount, b.taxpercent, b.producttax, b.productstock,
b.productcombo, b.variants, b.retailprice, b.diffprice, b.diffpercent, b.othercost, b.approve,
b.productstatus, b.created, b.updated, c.subcatname AS subcategoryname,
SUM(CASE WHEN a.stocktype = 'in' THEN a.quantity ELSE 0 END) -
SUM(CASE WHEN a.stocktype = 'out' THEN a.quantity ELSE 0 END) AS quantity
FROM productstocks a
JOIN products b ON a.productid = b.productid
INNER JOIN productsubcategories c ON c.subcatid = b.subcategoryid
`
if tenantID != "" {
conditions = append(conditions, "a.tenantid = ?")
params = append(params, tenantID)
}
if locationID != "" {
conditions = append(conditions, "a.locationid = ?")
params = append(params, locationID)
}
if len(conditions) > 0 {
query += " WHERE " + strings.Join(conditions, " AND ")
}
query += " GROUP BY a.productid"
if err := r.db.Raw(query, params...).Scan(&stocks).Error; err != nil {
return nil, err
}
return stocks, nil
}
func (r *productRepository) CreateProductStock(stocks []models.Productstock) error {
return r.db.Table("productstocks").Create(&stocks).Error
}
func (r *productRepository) UpdateProductStatus(productIDs []int, status string) error {
return r.db.Table("products").
Where("productid IN ?", productIDs).
Update("productstatus", status).Error
}
func (r *productRepository) CreateProduct(product models.Products) error {
tx := r.db.Begin()
if err := tx.Create(&product).Error; err != nil {
tx.Rollback()
return err
}
if err := tx.Commit().Error; err != nil {
return err
}
return nil
}
func (r *productRepository) UpdateProduct(product models.Products) error {
tx := r.db.Begin()
if err := tx.Table("productlocations").
Where("productid = ?", product.Productid).
Select("status"). // only update 'approve' field
Updates(product).Error; err != nil {
tx.Rollback()
return err
}
return tx.Commit().Error
}
func (r *productRepository) DeleteProduct(productID int) error {
tx := r.db.Begin()
if err := tx.Table("products").Where("productid = ?", productID).Delete(&models.Products{}).Error; err != nil {
tx.Rollback()
return err
}
if err := tx.Commit().Error; err != nil {
return err
}
return nil
}
func (r *productRepository) GetStockStatement(tenantID, locationID, subcategoryID, pageno, pagesize int, keyword string) ([]models.Productstockstatement, error) {
data := make([]models.Productstockstatement, 0)
if pageno < 1 {
pageno = 1
}
if pagesize < 1 {
pagesize = 10
}
offset := (pageno - 1) * pagesize
params := []interface{}{tenantID, locationID}
query := `SELECT a.productid,a.productname,a.productimage,a.categoryid,a.subcategoryid,a.productunit,a.unitvalue,a.productcost,a.taxpercent,a.taxamount,a.retailprice,b.tenantid,b.locationid,
COALESCE( SUM(CASE WHEN UPPER(c.stocktype) = 'IN' AND c.stockdate::date <= CURRENT_DATE THEN c.quantity ELSE 0 END) -
SUM(CASE WHEN UPPER(c.stocktype) = 'OUT' AND c.stockdate::date <= CURRENT_DATE THEN c.quantity ELSE 0 END),0 )
AS opening,
COALESCE(SUM(CASE WHEN UPPER(c.stocktype) = 'IN' AND c.stockdate::date = CURRENT_DATE THEN c.quantity ELSE 0 END), 0) AS credit,
COALESCE(SUM(CASE WHEN UPPER(c.stocktype) = 'OUT' AND c.stockdate::date = CURRENT_DATE THEN c.quantity ELSE 0 END), 0) AS debit,
COALESCE(
( SUM(CASE WHEN UPPER(c.stocktype) = 'IN' AND c.stockdate::date < CURRENT_DATE THEN c.quantity ELSE 0 END) -
SUM(CASE WHEN UPPER(c.stocktype) = 'OUT' AND c.stockdate::date < CURRENT_DATE THEN c.quantity ELSE 0 END)
) +
SUM(CASE WHEN UPPER(c.stocktype) = 'IN' AND c.stockdate::date = CURRENT_DATE THEN c.quantity ELSE 0 END) -
SUM(CASE WHEN UPPER(c.stocktype) = 'OUT' AND c.stockdate::date = CURRENT_DATE THEN c.quantity ELSE 0 END),
0
) AS closing
FROM products a
JOIN productlocations b ON a.productid = b.productid AND a.tenantid = b.tenantid
LEFT JOIN productstocks c ON a.productid = c.productid AND b.locationid = c.locationid AND b.tenantid = c.tenantid
WHERE b.tenantid = ? AND b.locationid = ?`
if subcategoryID != 0 {
query += " AND a.subcategoryid = ?"
params = append(params, subcategoryID)
}
if keyword != "" {
query += " AND (CAST(a.productid AS TEXT) LIKE ? OR LOWER(a.productname) LIKE ?)"
likeParam := "%" + strings.ToLower(keyword) + "%"
params = append(params, likeParam, likeParam)
}
query += `
GROUP BY
a.productid, a.productname, a.productimage,
a.categoryid, a.subcategoryid, a.productunit,
a.productcost, a.taxpercent, a.taxamount,
a.retailprice, b.tenantid, b.locationid
ORDER BY a.productid DESC LIMIT ` + strconv.Itoa(pagesize) + ` OFFSET ` + strconv.Itoa(offset)
if err := r.db.Raw(query, params...).Scan(&data).Error; err != nil {
return nil, err
}
print(query)
return data, nil
}
func (r *productRepository) GetLocationProducts(tenantID, locationID, subcategoryID, pageno, pagesize int, keyword string) ([]models.Locationproducts, error) {
var data []models.Locationproducts
if pageno < 1 {
pageno = 1
}
if pagesize < 1 {
pagesize = 10
}
offset := (pageno - 1) * pagesize
params := []interface{}{tenantID, locationID}
query := `SELECT a.*, b.productlocationid, b.status,
COALESCE(SUM(CASE WHEN UPPER(c.stocktype) = 'IN' THEN c.quantity ELSE 0 END), 0) AS total_in,
COALESCE(SUM(CASE WHEN UPPER(c.stocktype) = 'OUT' THEN c.quantity ELSE 0 END), 0) AS total_out,
COALESCE(SUM(CASE WHEN UPPER(c.stocktype) = 'IN' THEN c.quantity ELSE 0 END) -
SUM(CASE WHEN UPPER(c.stocktype) = 'OUT' THEN c.quantity ELSE 0 END), 0) AS productstock
FROM products a
INNER JOIN productlocations b ON a.productid = b.productid AND a.tenantid = b.tenantid
LEFT JOIN productstocks c ON a.productid = c.productid AND b.locationid = c.locationid AND a.tenantid = c.tenantid
WHERE a.approve=1 AND a.tenantid = ? AND b.locationid = ?`
if subcategoryID != 0 {
query += " AND a.subcategoryid = ?"
params = append(params, subcategoryID)
}
if keyword != "" {
query += " AND LOWER(a.productname) LIKE ?"
params = append(params, "%"+strings.ToLower(keyword)+"%")
}
query += ` GROUP BY a.productid, a.productname, a.productimage, a.categoryid, a.subcategoryid,
a.productunit, a.productcost, a.taxpercent, a.taxamount, a.retailprice,
b.tenantid, b.locationid, b.productlocationid, b.status
ORDER BY a.productid DESC LIMIT ? OFFSET ?`
params = append(params, pagesize, offset)
if err := r.db.Raw(query, params...).Scan(&data).Error; err != nil {
return nil, err
}
print(query)
return data, nil
}
func (r *productRepository) GetLocationProductSummary(tenantID, locationID int) ([]models.ProductSummary, error) {
data := make([]models.ProductSummary, 0)
query := `
SELECT
a.subcatid AS subcategoryid,
a.subcatname AS subcategroyname,
a.image,
COUNT(DISTINCT c.productid) AS productcount
FROM productsubcategories a
LEFT JOIN products b
ON a.subcatid = b.subcategoryid
AND b.approve = 1
AND b.tenantid = ?
LEFT JOIN productlocations c
ON b.productid = c.productid
AND c.tenantid = ?
AND c.locationid = ?
WHERE a.categoryid = 2
GROUP BY a.subcatid, a.subcatname, a.image
ORDER BY a.subcatid;
`
// Only 3 params: tenantID for products, tenantID for locations, locationID
params := []interface{}{tenantID, tenantID, locationID}
if err := r.db.Raw(query, params...).Scan(&data).Error; err != nil {
return nil, err
}
// Correct "All" count: sum of only products that exist in this location
total := 0
for _, d := range data {
total += d.Productcount
}
all := models.ProductSummary{
Subcategoryid: 0,
Subcategroyname: "All",
Productcount: total,
}
data = append([]models.ProductSummary{all}, data...)
return data, nil
}
func (r *productRepository) FetchFilteredProducts(
categoryID, subcategoryID, productID, applocationID, tenantID,
locationID int, keyword, productStatus, approve string, pageno, pagesize int,
) ([]models.Tenantproducts, error) {
offset := (pageno - 1) * pagesize
results := make([]models.Tenantproducts, 0)
if tenantID == 0 {
return results, nil
}
// Fetch tenant info
var tenant models.TenantInfo
if err := r.db.Table("tenants").Where("tenantid = ?", tenantID).First(&tenant).Error; err != nil {
return nil, err
}
// Build product query
var products []models.Products
query := r.db.
Table("products a").
Select(`
a.*,
b.status,
c.categoryname,
d.subcatname AS subcategoryname,
ps.locationid,
COALESCE(ps.quantity, 0) AS quantity
`).
Joins("LEFT JOIN productlocations b ON a.productid = b.productid").
Joins("LEFT JOIN productcategories c ON a.categoryid = c.categoryid").
Joins("LEFT JOIN productsubcategories d ON a.subcategoryid = d.subcatid").
Joins(`
LEFT JOIN (
SELECT
productid,
locationid,
SUM(CASE WHEN stocktype = 'in' THEN quantity ELSE 0 END) -
SUM(CASE WHEN stocktype = 'out' THEN quantity ELSE 0 END) AS quantity
FROM productstocks
GROUP BY productid, locationid
) ps ON ps.productid = a.productid
`).
Where("a.tenantid = ?", tenantID).
Order("a.productid DESC")
if categoryID != 0 {
query = query.Where("a.categoryid = ?", categoryID)
}
if subcategoryID != 0 {
query = query.Where("a.subcategoryid = ?", subcategoryID)
}
if productID != 0 {
query = query.Where("a.productid = ?", productID)
}
if productStatus != "" {
query = query.Where("a.productstatus = ?", productStatus)
}
if locationID != 0 {
query = query.Where("e.locationid = ?", locationID)
}
if approve != "" {
query = query.Where("a.approve = ?", approve)
}
if keyword != "" {
like := "%" + strings.ToLower(keyword) + "%"
query = query.Where(
r.db.Where("LOWER(a.productname) LIKE ?", like).
Or("LOWER(a.unitvalue) LIKE ?", like).
Or("LOWER(CAST(a.productcost AS TEXT)) LIKE ?", like),
)
}
if pagesize > 0 && offset >= 0 {
query = query.Limit(pagesize).Offset(offset)
}
if err := query.Scan(&products).Error; err != nil {
return nil, err
}
if products == nil {
products = []models.Products{}
}
results = append(results, models.Tenantproducts{
Tenant: tenant,
Products: products,
})
print(query)
return results, nil
}
func (r *productRepository) GetProductByVariant(tenantid, variantid int) ([]models.Products, error) {
var data []models.Products
err := r.db.
Table("products p").
Select(`
p.*,
c.categoryname,
d.subcatname AS subcategoryname,
COALESCE(pd.discountvalue, 0) AS discountvalue,
pd.discountid
`).
Joins("LEFT JOIN productcategories c ON p.categoryid = c.categoryid").
Joins("LEFT JOIN productsubcategories d ON p.subcategoryid = d.subcatid").
Joins("LEFT JOIN productdiscounts pd ON pd.productid = p.productid").
Where("p.tenantid = ? AND p.variants = ?", tenantid, variantid).
Order("p.productid DESC").
Scan(&data).Error
if err != nil {
return nil, err
}
return data, nil
}
func (r *productRepository) GetSubcategories(categoryID int) ([]models.Subcategory, error) {
var subcats []models.Subcategory
err := r.db.Table("productsubcategories").
Where("categoryid = ?", categoryID).
Find(&subcats).Error
return subcats, err
}
func (r *productRepository) GetProducts(params models.ProductFilter) ([]models.Products, error) {
var products []models.Products
q := r.db.Table("products a").
Joins("LEFT JOIN productlocations pl ON pl.productid = a.productid").
Joins("LEFT JOIN productdiscounts pd ON pd.productid = a.productid").
Joins("LEFT JOIN productcategories c ON a.categoryid = c.categoryid").
Where("a.categoryid = ?", params.CategoryID)
if params.TenantID > 0 {
q = q.Where("a.tenantid = ?", params.TenantID)
}
if params.LocationID > 0 {
q = q.Where("pl.locationid = ?", params.LocationID)
}
if params.AppLocationID > 0 {
q = q.Where("a.applocationid = ?", params.AppLocationID)
}
if params.ProductID > 0 {
q = q.Where("a.productid = ?", params.ProductID)
}
if params.Keyword != "" {
like := "%" + strings.ToLower(params.Keyword) + "%"
q = q.Where(
r.db.Where("LOWER(a.productname) LIKE ?", like).
Or("LOWER(a.unitvalue) LIKE ?", like).
Or("LOWER(CAST(a.productcost AS TEXT)) LIKE ?", like),
)
}
err := q.Select(`
a.*,
COALESCE(pd.discountvalue, 0) AS discountvalue
`).Find(&products).Error
return products, err
}
func (r *productRepository) GetTenantInfo(tenantID, applocationID int) (map[string]interface{}, error) {
var tenant struct {
Tenantname string
Address string
Licenseno string
Primaryemail string
Primarycontact string
Locationname string
Pickuplocationid int
Suburb string
City string
Latitude string
Longitude string
Postcode string
}
err := r.db.Raw(`
SELECT t.tenantname, t.address, t.licenseno, t.primaryemail, t.primarycontact,
l.locationid AS pickuplocationid, l.suburb, l.city, l.latitude, l.longitude, l.postcode,
a.locationname
FROM tenants t
LEFT JOIN tenantlocations l ON t.tenantid = l.tenantid
LEFT JOIN app_location a ON l.applocationid = a.applocationid
WHERE t.tenantid = ? AND t.applocationid = ?
LIMIT 1
`, tenantID, applocationID).Scan(&tenant).Error
if err != nil {
return nil, err
}
return map[string]interface{}{
"tenantname": tenant.Tenantname,
"address": tenant.Address,
"licenseno": tenant.Licenseno,
"primaryemail": tenant.Primaryemail,
"primarycontact": tenant.Primarycontact,
"locationname": tenant.Locationname,
"pickuplocationid": tenant.Pickuplocationid,
"suburb": tenant.Suburb,
"city": tenant.City,
"pickuplat": tenant.Latitude,
"pickuplong": tenant.Longitude,
"postcode": tenant.Postcode,
}, nil
}
func (r *productRepository) UpdateProductLocation(input models.Productlocations) error {
tx := r.db.Begin()
t1 := tx.Where("productlocationid = ?", input.Productlocationid).Updates(&input)
if t1.Error != nil {
tx.Rollback()
return t1.Error
}
if err := tx.Commit().Error; err != nil {
return err
}
return nil
}
func (r *productRepository) CreateProductLocation(input []models.Productlocations) error {
var stk []models.Productstock
tx := r.db.Begin()
// Insert or update product location
if err := tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "tenantid"}, {Name: "locationid"}, {Name: "productid"}},
DoUpdates: clause.AssignmentColumns([]string{"price", "minquantity", "maxquantity", "status"}),
}).Create(&input).Error; err != nil {
tx.Rollback()
return err
}
// Prepare product stock entries
for _, loc := range input {
if loc.Quantity > 0 {
stk = append(stk, models.Productstock{
Tenantid: loc.Tenantid,
Stockdate: time.Now(),
Locationid: loc.Locationid,
Productid: loc.Productid,
Quantity: loc.Quantity,
Stocktype: loc.Stocktype,
})
}
}
// Insert stock records if available
if len(stk) > 0 {
if err := tx.Create(&stk).Error; err != nil {
tx.Rollback()
return err
}
}
// Commit transaction
if err := tx.Commit().Error; err != nil {
return err
}
return nil
}
func (r *productRepository) CreateProductVariant(input models.Productvariant) error {
tx := r.db.Begin()
if err := tx.Create(&input).Error; err != nil {
tx.Rollback()
return err
}
if err := tx.Commit().Error; err != nil {
return err
}
return nil
}
func (r *productRepository) DeleteProductLocation(tenantid, locationid, productid int) error {
if err := r.db.Where("tenantid = ? AND locationid = ? AND productid = ?", tenantid, locationid, productid).Delete(&models.Productlocations{}).Error; err != nil {
return err
}
return nil
}
// FindTenantProductByCatalogueRef looks up the tenant's existing snapshot of
// a catalogue product, keyed on (tenantid, brand, catalogueid) since a
// catalogue row's bare id is only unique within its own brand table.
func (r *productRepository) FindTenantProductByCatalogueRef(tenantid int, brand string, catalogueid int64) (*models.Products, error) {
var product models.Products
result := r.db.Table("products").
Where("tenantid = ? AND productbrand = ? AND catalogueid = ?", tenantid, brand, catalogueid).
First(&product)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, result.Error
}
return &product, nil
}
// CreateProductReturningID inserts a new product snapshot and returns its
// generated productid. Kept separate from CreateProduct so existing callers
// of CreateProduct are unaffected.
func (r *productRepository) CreateProductReturningID(product models.Products) (int, error) {
if err := r.db.Create(&product).Error; err != nil {
return 0, err
}
return product.Productid, nil
}
// GetImportedCatalogueRefs returns the (brand, catalogueid) pairs this
// tenant has already imported, so a catalogue browse screen can mark items
// as already-imported without diffing full product lists client-side. Brand
// is optional: omitted, it covers every brand at once — needed because the
// all-brands browse view mixes products whose bare catalogueid can collide
// across brand tables, so brand must travel with every id.
func (r *productRepository) GetImportedCatalogueRefs(tenantid int, brand string) ([]models.ImportedCatalogueRef, error) {
refs := make([]models.ImportedCatalogueRef, 0)
query := r.db.Table("products").
Select("productbrand AS brand, catalogueid").
Where("tenantid = ? AND catalogueid IS NOT NULL AND catalogueid != 0", tenantid)
if brand != "" {
query = query.Where("productbrand = ?", brand)
}
err := query.Scan(&refs).Error
return refs, err
}
// UpdateProductPricing updates only the pricing fields on a product
// snapshot, used when a catalogue product is re-imported with new pricing.
func (r *productRepository) UpdateProductPricing(productid int, retailprice, productcost, taxpercent float64) error {
return r.db.Table("products").
Where("productid = ?", productid).
Updates(map[string]interface{}{
"retailprice": retailprice,
"productcost": productcost,
"taxpercent": taxpercent,
}).Error
}