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 ReactivateProductLocations(refs []models.ProductLocationRef) 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, locationid 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 } // ReactivateProductLocations flips productlocations.status back to // "available" for each ref — the counterpart to CreateOrder flagging a // location "outofstock" when its stock hits zero. Without this, a store // that runs out and then restocks via an approved stock request stays // flagged outofstock forever, since receiving stock only ever added to the // productstocks ledger and never touched this per-location flag. func (r *productRepository) ReactivateProductLocations(refs []models.ProductLocationRef) error { for _, ref := range refs { if err := r.db.Table("productlocations"). Where("tenantid = ? AND locationid = ? AND productid = ?", ref.Tenantid, ref.Locationid, ref.Productid). Update("status", "available").Error; err != nil { return err } } return nil } 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} // b.price is the per-store selling price. It has to be selected explicitly: // a.* only covers products (whose price column is retailprice, the master // price), so without this the catalogue could never read back a price set // for this outlet via CreateProductLocation. // quantity/productstock both alias the same live SUM(in)-SUM(out) balance // from productstocks — placed after a.* so they overwrite the static, // never-decremented products.quantity column GORM would otherwise scan // into Locationproducts.Quantity, which is what made the console's stock // column look frozen after an order despite CreateOrder recording the // "out" ledger entry correctly. query := `SELECT a.*, b.productlocationid, b.status, b.price, 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, 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 quantity 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, b.price 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, locationid int) ([]models.Products, error) { var data []models.Products // productstock/quantity are correlated subqueries (not a JOIN+GROUP BY) so // they can coexist with `p.*` without having to enumerate every products // column. quantity is duplicated on purpose: it's placed after `p.*` so it // overwrites the static, never-decremented products.quantity column that // would otherwise scan into Products.Quantity. When locationid is 0 // (caller didn't scope to a store), both subqueries and the // productlocations join simply match nothing, so // Productstock/Quantity/Locationstatus come back zero-valued — same // response shape as before this field existed, not an error. err := r.db. Table("products p"). Select(` p.*, c.categoryname, d.subcatname AS subcategoryname, COALESCE(pd.discountvalue, 0) AS discountvalue, pd.discountid, pl.status AS locationstatus, COALESCE(( SELECT SUM(CASE WHEN LOWER(ps.stocktype) = 'in' THEN ps.quantity ELSE 0 END) - SUM(CASE WHEN LOWER(ps.stocktype) = 'out' THEN ps.quantity ELSE 0 END) FROM productstocks ps WHERE ps.productid = p.productid AND ps.tenantid = p.tenantid AND ps.locationid = ? ), 0) AS productstock, COALESCE(( SELECT SUM(CASE WHEN LOWER(ps.stocktype) = 'in' THEN ps.quantity ELSE 0 END) - SUM(CASE WHEN LOWER(ps.stocktype) = 'out' THEN ps.quantity ELSE 0 END) FROM productstocks ps WHERE ps.productid = p.productid AND ps.tenantid = p.tenantid AND ps.locationid = ? ), 0) AS quantity `, locationid, locationid). 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"). Joins("LEFT JOIN productlocations pl ON pl.productid = p.productid AND pl.tenantid = p.tenantid AND pl.locationid = ?", locationid). 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), ) } // productstock/quantity are correlated subqueries computing the live // SUM(in)-SUM(out) balance from productstocks, scoped to params.LocationID // (0 if the caller didn't scope to a store, matching nothing so both come // back zero). quantity is placed after `a.*` so it overwrites the static, // never-decremented products.quantity column — same fix as // GetLocationProducts/GetProductByVariant, otherwise this endpoint would // keep showing stock that never reduces after an order. err := q.Select(` a.*, COALESCE(pd.discountvalue, 0) AS discountvalue, COALESCE(( SELECT SUM(CASE WHEN LOWER(ps.stocktype) = 'in' THEN ps.quantity ELSE 0 END) - SUM(CASE WHEN LOWER(ps.stocktype) = 'out' THEN ps.quantity ELSE 0 END) FROM productstocks ps WHERE ps.productid = a.productid AND ps.tenantid = a.tenantid AND ps.locationid = ? ), 0) AS productstock, COALESCE(( SELECT SUM(CASE WHEN LOWER(ps.stocktype) = 'in' THEN ps.quantity ELSE 0 END) - SUM(CASE WHEN LOWER(ps.stocktype) = 'out' THEN ps.quantity ELSE 0 END) FROM productstocks ps WHERE ps.productid = a.productid AND ps.tenantid = a.tenantid AND ps.locationid = ? ), 0) AS quantity `, params.LocationID, params.LocationID).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 }