Files
backend_jupiter/domain/product.go
2026-05-25 11:45:56 +05:30

848 lines
22 KiB
Go

package domain
import (
"errors"
"nearle/db"
"nearle/models"
"nearle/utils"
"strconv"
"strings"
"time"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
func GetProductCategory() []models.ProductCategory {
var data []models.ProductCategory
q1 := `SELECT * FROM productcategories WHERE moduleid = 2 and status = "Active"`
db.DB.Raw(q1).Scan(&data)
return data
}
func GetProductSubCategory(categoryID, tenantID int) []models.ProductSubCategory {
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)
}
db.DB.Raw(query.String(), args...).Scan(&data)
return data
}
func CreateProduct(product *models.Products) error {
tx := db.DB.Begin()
t1 := tx.Create(&product)
if t1.Error != nil {
utils.Error("CreateProduct error", "error", t1.Error)
tx.Rollback()
return t1.Error
}
err := tx.Commit().Error
if err != nil {
return err
}
return nil
}
func CreateProductVariant(input models.Productvariant) error {
tx := db.DB.Begin()
t1 := tx.Create(&input)
if t1.Error != nil {
utils.Error("CreateProductVariant error", "error", t1.Error)
tx.Rollback()
return t1.Error
}
err := tx.Commit().Error
if err != nil {
return err
}
return nil
}
func 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
}
var tenant models.TenantInfo
err := db.DB.Table("tenants").Where("tenantid = ?", tenantID).First(&tenant).Error
if err != nil {
return nil, err
}
var products []models.Products
query := db.DB.
Debug().
Table("products a").
Select(`
a.*,
c.categoryname,
d.subcatname AS subcategoryname,
e.locationid,
COALESCE(
SUM(CASE WHEN e.stocktype = 'in' THEN e.quantity ELSE 0 END) -
SUM(CASE WHEN e.stocktype = 'out' THEN e.quantity ELSE 0 END),
0
) AS quantity
`).
Joins("LEFT JOIN productcategories c ON a.categoryid = c.categoryid").
Joins("LEFT JOIN productsubcategories d ON a.subcategoryid = d.subcatid").
Joins("LEFT JOIN productstocks e ON e.productid = a.productid").
Where("a.tenantid = ?", tenantID).
Group("a.productid"). // Important for SUM
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(
db.DB.Where("LOWER(a.productname) LIKE ?", like).
Or("LOWER(a.unitvalue) LIKE ?", like).
Or("LOWER(CAST(a.productcost AS CHAR)) LIKE ?", like),
)
}
if pagesize > 0 && offset >= 0 {
query = query.Limit(pagesize).Offset(offset)
}
err = query.Scan(&products).Error
if err != nil {
return nil, err
}
if products == nil {
products = []models.Products{}
}
results = append(results, models.Tenantproducts{
Tenant: tenant,
Products: products,
})
return results, nil
}
func UpdateProduct(input models.Products) error {
tx := db.DB.Begin()
t1 := tx.Where("productid=?", input.Productid).Updates(&input)
if t1.Error != nil {
tx.Rollback()
return t1.Error
}
err := tx.Commit().Error
if err != nil {
return err
}
return nil
}
func 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 := db.DB.Raw(baseQuery, params...).Scan(&data).Error; err != nil {
return nil, err
}
return data, nil
}
func GetproductbyVariant(tenantid, variantid int) ([]models.Products, error) {
var data []models.Products
err := db.DB.
Table("products p").
Select("p.*, c.categoryname, d.subcatname as subcategoryname").
Joins("LEFT JOIN productcategories c ON p.categoryid = c.categoryid").
Joins("LEFT JOIN productsubcategories d ON p.subcategoryid = d.subcatid").
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 GetProductVariants(tenantId int, subcategoryId int) []models.Productvariant {
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)
}
db.DB.Raw(query, params...).Scan(&data)
return data
}
func GetCatalougeProducts(tenantId, locationid, subcategoryid, pageno, pagesize int, keyword string) []models.Products {
var data []models.Products
var query string
if pageno < 1 {
pageno = 1
}
if pagesize < 1 {
pagesize = 10
}
offset := (pageno - 1) * pagesize
params := []interface{}{locationid, tenantId}
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 order by a.productid desc LIMIT ` + strconv.Itoa(pagesize) + ` OFFSET ` + strconv.Itoa(offset)
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)+"%")
}
db.DB.Raw(query, params...).Scan(&data)
utils.Logger.Debugf("Query: %s", query)
return data
}
func GetLocationProducts(tenantid, locationid, subcategoryid, pageno, pagesize int, keyword string) []models.Products {
var data []models.Products
if pageno < 1 {
pageno = 1
}
if pagesize < 1 {
pagesize = 10
}
offset := (pageno - 1) * pagesize
params := []interface{}{tenantid, locationid}
query := `SELECT a.*,
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`
query += " ORDER BY a.productid DESC LIMIT ? OFFSET ?"
params = append(params, pagesize, offset)
db.DB.Raw(query, params...).Scan(&data)
return data
}
func GetStockStatement(tenantid, locationid, subcategoryid, pageno, pagesize int, keyword string) []models.Productstockstatement {
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 CHAR) 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)
db.DB.Raw(query, params...).Scan(&data)
utils.Logger.Debugf("Query: %s", query)
return data
}
func CreateProductLocation(input []models.Productlocations) error {
var stk []models.Productstock
tx := db.DB.Begin()
if err := tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "tenantid"}, {Name: "locationid"}, {Name: "productid"}},
DoUpdates: clause.AssignmentColumns([]string{"price", "minquantity", "maxquantity"}),
}).Create(&input).Error; err != nil {
tx.Rollback()
return err
}
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,
})
}
}
if len(stk) > 0 {
t2 := tx.Create(&stk)
if t2.Error != nil {
tx.Rollback()
return t2.Error
}
}
err := tx.Commit().Error
if err != nil {
return err
}
return nil
}
func UpdateProductLocation(input models.Productlocations) error {
tx := db.DB.Begin()
t1 := tx.Where("productlocationid=?", input.Productlocationid).Updates(&input)
if t1.Error != nil {
tx.Rollback()
return t1.Error
}
err := tx.Commit().Error
if err != nil {
return err
}
return nil
}
func FetchProductsBySubcategory(categoryID, tenantID, applocationID, locationID, productID int, keyword string) (map[string]interface{}, error) {
utils.Logger.Infof("Domain: Fetching products for categoryID=%d, tenantID=%d", categoryID, tenantID)
// Step 1: Fetch subcategories
var subcategories []models.Subcategory
err := db.DB.Table("productsubcategories").
Where("categoryid = ?", categoryID).
Find(&subcategories).Error
if err != nil {
utils.Error("Error fetching subcategories", "error", err)
return nil, err
}
// Step 2: Build product query
var products []models.Products
query := db.DB.Table("products a").
Joins("LEFT JOIN productlocations pl ON pl.productid = a.productid").
Where("a.categoryid = ? AND a.tenantid = ?", categoryID, tenantID)
if locationID > 0 {
query = query.Where("pl.locationid = ?", locationID)
}
if applocationID > 0 {
query = query.Where("a.applocationid = ?", applocationID)
}
if productID > 0 {
query = query.Where("a.productid = ?", productID)
}
if keyword != "" {
like := "%" + strings.ToLower(keyword) + "%"
query = query.Where(
db.DB.Where("LOWER(a.productname) LIKE ?", like).
Or("LOWER(a.unitvalue) LIKE ?", like).
Or("LOWER(CAST(a.productcost AS CHAR)) LIKE ?", like),
)
}
if err := query.Select("a.*").Find(&products).Error; err != nil {
utils.Error("Error fetching products", "error", err)
return nil, err
}
// Step 3: Grouping
var grouped []models.SubcategoryProductResponse
var uncategorized []models.Products
for _, subcat := range subcategories {
var matched []models.Products
for _, prod := range products {
if prod.Subcategoryid != nil && *prod.Subcategoryid == subcat.Subcategoryid {
matched = append(matched, prod)
}
}
if len(matched) > 0 {
grouped = append(grouped, models.SubcategoryProductResponse{
SubcategoryID: subcat.Subcategoryid,
SubcategoryName: subcat.Subcategoryname,
Products: matched,
})
}
}
// Add uncategorized
for _, p := range products {
if p.Subcategoryid == nil {
uncategorized = append(uncategorized, p)
}
}
if len(uncategorized) > 0 {
grouped = append(grouped, models.SubcategoryProductResponse{
SubcategoryID: 0,
SubcategoryName: "Uncategorized",
Products: uncategorized,
})
}
// Step 4: Tenant info
var tenantInfo map[string]interface{}
for _, p := range products {
if p.Tenantid != nil {
tenantInfo, err = fetchTenantDetails(*p.Tenantid, applocationID)
if err != nil {
utils.Error("Error fetching tenant details", "error", err)
}
break
}
}
if tenantInfo == nil {
return map[string]interface{}{}, nil
}
tenantInfo["details"] = grouped
return tenantInfo, nil
}
func fetchTenantDetails(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 := db.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 && !errors.Is(err, gorm.ErrRecordNotFound) {
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 UpdateProductStock(stock *models.Productstock) error {
// Ensure the record exists
var existing models.Productstock
if err := db.DB.First(&existing, "productstockid = ?", stock.Productstockid).Error; err != nil {
return err
}
// Update fields
return db.DB.Model(&existing).Updates(models.Productstock{
Tenantid: stock.Tenantid,
Stockdate: stock.Stockdate,
Locationid: stock.Locationid,
Productid: stock.Productid,
Quantity: stock.Quantity,
Stocktype: stock.Stocktype,
Status: stock.Status,
}).Error
}
func 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 := db.DB.Raw(query, params...).Scan(&stocks).Error; err != nil {
return nil, err
}
return stocks, nil
}
func GetSubCategoryWiseSummary(tenantId, locationid, subcategoryid int) ([]models.SubCategorySummary, error) {
var data []models.SubCategorySummary
query := `
SELECT a.subcategoryid AS subcategoryid, c.subcatname AS subcatname, COUNT(a.productid) AS productcount
FROM products a
LEFT JOIN productlocations b ON a.productid = b.productid AND b.locationid = ? AND b.tenantid = a.tenantid
LEFT JOIN productsubcategories c ON a.subcategoryid = c.subcatid
WHERE a.approve = 1 AND a.tenantid = ? AND b.productid IS NULL
`
var params []interface{}
params = append(params, locationid, tenantId)
if subcategoryid > 0 {
query += " AND a.subcategoryid = ?"
params = append(params, subcategoryid)
}
query += `
GROUP BY a.subcategoryid, c.subcatname
ORDER BY a.subcategoryid ASC
`
if err := db.DB.Raw(query, params...).Scan(&data).Error; err != nil {
return nil, err
}
return data, nil
}
func GetLocationProductSummary(tenantid, locationid int) []models.ProductSummary {
data := make([]models.ProductSummary, 0)
params := []interface{}{tenantid, locationid}
query := `
SELECT a.subcatid AS subcategoryid, a.subcatname AS subcategroyname, a.image, COUNT(DISTINCT b.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 b.tenantid = c.tenantid AND c.locationid = ?
WHERE a.categoryid = 2
GROUP BY a.subcatid, a.subcatname
ORDER BY a.subcatid;
`
db.DB.Raw(query, params...).Scan(&data)
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
}
func GetStockStatementSummary(tenantid, locationid int) []models.ProductSummary {
data := make([]models.ProductSummary, 0)
params := []interface{}{tenantid, locationid}
query := `
SELECT a.subcatid AS subcategoryid, a.subcatname AS subcategroyname, a.image, COUNT(DISTINCT b.productid) AS productcount
FROM productsubcategories a
LEFT JOIN products b ON a.subcatid = b.subcategoryid AND b.tenantid = ?
LEFT JOIN productlocations c ON b.productid = c.productid AND b.tenantid = c.tenantid AND c.locationid = ?
WHERE a.categoryid = 2
GROUP BY a.subcatid, a.subcatname
ORDER BY a.subcatid;
`
db.DB.Raw(query, params...).Scan(&data)
utils.Logger.Debugf("Query: %s", query)
// Add "All" row
total := models.ProductSummary{
Subcategoryid: 0,
Subcategroyname: "All",
}
for _, d := range data {
total.Productcount += d.Productcount
}
data = append([]models.ProductSummary{total}, data...)
return data
}
func CreateProductDiscount(discount *models.ProductDiscount) error {
// convert []string to comma-separated string
if len(discount.Locationid) > 0 {
discount.LocationidStr = strings.Join(discount.Locationid, ",")
}
tx := db.DB.Begin()
if err := tx.Table("productdiscounts").Create(discount).Error; err != nil {
tx.Rollback()
return err
}
return tx.Commit().Error
}
func GetProductDiscounts(tid int, lid string) []models.ProductDiscount {
var result []models.ProductDiscount
query := `
SELECT
discountid, discounttypeid, tenantid, productid, moduleid,
locationid, discountname, discountcode,
discountterms, discountvalue, startdate, enddate, status
FROM productdiscounts
WHERE tenantid = ?
AND moduleid = 2
AND FIND_IN_SET(?, locationid)
`
db.DB.Raw(query, tid, lid).Scan(&result)
// convert comma separated location ids to []string
for i := range result {
if result[i].LocationidStr != "" {
result[i].Locationid = strings.Split(result[i].LocationidStr, ",")
} else {
result[i].Locationid = []string{}
}
}
return result
}