subcategoryid is no longer required to import a catalogue product — it's a display/grouping hint elsewhere (the codebase already has an "Uncategorized" fallback for subcategoryid=0), so requiring it was pure friction with no correctness payoff. Fixes the category picker at the root: categoryid 2, which tenant 1135's real products actually use, has no row in productcategories at all (not a filter bug — the master data is genuinely missing it). Rather than inventing category master data, adds GET /products/gettenantcategories, which lists categories a tenant's own products actually use (falling back to a synthesized label when the master table has no name), so the import category picker always offers something real instead of an incomplete global list. Also relaxes the subcategory lookup's tenant filter to include unowned/global rows (tenantid NULL or 0), not just exact tenant matches — categoryid 2's real subcategories carry no tenant at all, so the strict filter was hiding them even when a tenant wanted one. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
847 lines
26 KiB
Go
847 lines
26 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)
|
|
GetTenantCategories(tenantid int) ([]models.TenantCategory, 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{}
|
|
|
|
// tenantid is selected via COALESCE (not SELECT *) because the relaxed
|
|
// filter below can now return rows where it's NULL, which won't scan
|
|
// into the model's non-pointer int field otherwise.
|
|
query.WriteString(`SELECT subcatid, categoryid, COALESCE(tenantid, 0) AS tenantid,
|
|
subcatname, image, status, sortorder, createdby, created, updated
|
|
FROM productsubcategories WHERE 1=1`)
|
|
|
|
if tenantID != 0 {
|
|
// Some subcategories are tenant-owned overrides, others are shared
|
|
// master data with no tenant attached (tenantid NULL/0) — match both
|
|
// so a tenant sees the global set in addition to their own.
|
|
query.WriteString(" AND (tenantid = ? OR tenantid IS NULL OR tenantid = 0)")
|
|
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
|
|
}
|
|
|
|
// GetTenantCategories returns the distinct categoryids this tenant's own
|
|
// products actually use, LEFT JOINed against productcategories for a name
|
|
// (falling back to a synthesized label when that master row is missing —
|
|
// it's incomplete in practice, e.g. categoryid 2 has no productcategories
|
|
// row despite being in real use). This is deliberately not the global
|
|
// productcategories list: that list can omit categoryids tenants actually
|
|
// have products in, which would make the import category picker unusable.
|
|
func (r *productRepository) GetTenantCategories(tenantid int) ([]models.TenantCategory, error) {
|
|
categories := make([]models.TenantCategory, 0)
|
|
err := r.db.Raw(`
|
|
SELECT DISTINCT p.categoryid,
|
|
COALESCE(NULLIF(pc.categoryname, ''), 'Category ' || p.categoryid) AS categoryname
|
|
FROM products p
|
|
LEFT JOIN productcategories pc ON pc.categoryid = p.categoryid
|
|
WHERE p.tenantid = ? AND p.categoryid != 0
|
|
ORDER BY categoryname
|
|
`, tenantid).Scan(&categories).Error
|
|
return categories, 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
|
|
}
|