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 }