package services import ( "fmt" "nearle/models" "nearle/repositories" "time" ) type ProductService 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) UpdateProductStatus(productIDs []int, status string) error CreateProductStock(stocks []models.Productstock) 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) GetSaleTemplate(tenantID, locationID int) (*models.SaleTemplate, 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) GetProductsBySubcategory(params models.ProductFilter) (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 ImportCatalogueProduct(reqs []models.ImportCatalogueProductRequest) error GetImportedCatalogueRefs(tenantid int, brand string) ([]models.ImportedCatalogueRef, error) GetTenantCategories(tenantid int) ([]models.TenantCategory, error) } type productService struct { repo repositories.ProductRepository catalogueService CatalogueService } func NewProductService(repo repositories.ProductRepository, catalogueService CatalogueService) ProductService { return &productService{repo: repo, catalogueService: catalogueService} } func (s *productService) GetProductSubCategory(categoryID, tenantID int) ([]models.ProductSubCategory, error) { return s.repo.GetProductSubCategory(categoryID, tenantID) } func (s *productService) GetProductCount(tenantID, categoryID, subcategoryID int, approve string) ([]models.Productcount, error) { return s.repo.GetProductCount(tenantID, categoryID, subcategoryID, approve) } func (s *productService) GetProductCategory() ([]models.ProductCategory, error) { return s.repo.GetProductCategory() } func (s *productService) GetProductVariants(tenantID, subcategoryID int) ([]models.Productvariant, error) { return s.repo.GetProductVariants(tenantID, subcategoryID) } func (s *productService) GetCatalougeProducts(tenantID, locationID, subcategoryID, pageno, pagesize int, keyword string) ([]models.Products, error) { return s.repo.GetCatalougeProducts(tenantID, locationID, subcategoryID, pageno, pagesize, keyword) } func (s *productService) GetProductStocks(tenantID, locationID string) ([]models.Productstocks, error) { return s.repo.GetProductStocks(tenantID, locationID) } func (s *productService) CreateProductStock(stocks []models.Productstock) error { for i := range stocks { stocks[i].Stockdate = time.Now() } if err := s.repo.CreateProductStock(stocks); err != nil { return err } locMap := make(map[models.ProductLocationRef]struct{}) var locRefs []models.ProductLocationRef for _, stk := range stocks { // Every entry gets synced, "in" and "out" alike: the status is now // derived from the resulting balance rather than assumed from the // direction of the movement, so an "out" that empties a location // flags it outofstock and a partial "in" that leaves the balance at // or below zero correctly does not mark it sellable. if stk.Productid > 0 && stk.Locationid > 0 && stk.Tenantid > 0 { ref := models.ProductLocationRef{Tenantid: stk.Tenantid, Locationid: stk.Locationid, Productid: stk.Productid} if _, exists := locMap[ref]; !exists { locMap[ref] = struct{}{} locRefs = append(locRefs, ref) } } } // products.productstatus is deliberately NOT touched here. It is a // per-product lifecycle field holding "Active"/"Inactive", and receiving // stock used to overwrite it with "available" — an availability value in a // lifecycle column, which is how 136 products ended up reading "available" // and 12 "outofstock" with their real lifecycle state destroyed. // // Availability is a per-outlet fact and belongs to productlocations.status, // which SyncProductLocationStatus derives from the ledger below. A single // column on products cannot express it anyway: the same product can be // stocked at one outlet and empty at another. if len(locRefs) > 0 { if err := s.repo.SyncProductLocationStatus(locRefs); err != nil { return err } } return nil } func (s *productService) UpdateProductStatus(productIDs []int, status string) error { return s.repo.UpdateProductStatus(productIDs, status) } func (s *productService) CreateProduct(product models.Products) error { return s.repo.CreateProduct(product) } func (s *productService) UpdateProduct(product models.Products) error { return s.repo.UpdateProduct(product) } func (s *productService) DeleteProduct(productID int) error { return s.repo.DeleteProduct(productID) } func (s *productService) GetStockStatement(tenantID, locationID, subcategoryID, pageno, pagesize int, keyword string) ([]models.Productstockstatement, error) { return s.repo.GetStockStatement(tenantID, locationID, subcategoryID, pageno, pagesize, keyword) } func (s *productService) GetLocationProducts(tenantID, locationID, subcategoryID, pageno, pagesize int, keyword string) ([]models.Locationproducts, error) { return s.repo.GetLocationProducts(tenantID, locationID, subcategoryID, pageno, pagesize, keyword) } func (s *productService) GetLocationProductSummary(tenantID, locationID int) ([]models.ProductSummary, error) { return s.repo.GetLocationProductSummary(tenantID, locationID) } func (s *productService) GetSaleTemplate(tenantID, locationID int) (*models.SaleTemplate, error) { return s.repo.GetSaleTemplate(tenantID, locationID) } func (s *productService) FetchFilteredProducts(categoryID, subcategoryID, productID, applocationID, tenantID, locationID int, keyword, productStatus, approve string, pageno, pagesize int) ([]models.Tenantproducts, error) { return s.repo.FetchFilteredProducts(categoryID, subcategoryID, productID, applocationID, tenantID, locationID, keyword, productStatus, approve, pageno, pagesize) } func (s *productService) GetProductByVariant(tenantid, variantid, locationid int) ([]models.Products, error) { var data []models.Products result, err := s.repo.GetProductByVariant(tenantid, variantid, locationid) if err != nil { return nil, err } data = result return data, nil } func (s *productService) GetProductsBySubcategory(params models.ProductFilter) (map[string]interface{}, error) { subcategories, err := s.repo.GetSubcategories(params.CategoryID) if err != nil { return nil, err } products, err := s.repo.GetProducts(params) if err != nil { return nil, err } var details []models.SubcategoryProductResponse var uncategorized []models.Products for _, sub := range subcategories { var subProducts []models.Products for _, p := range products { if p.Subcategoryid == sub.Subcategoryid { subProducts = append(subProducts, p) } } if len(subProducts) > 0 { details = append(details, models.SubcategoryProductResponse{ SubcategoryID: sub.Subcategoryid, SubcategoryName: sub.Subcategoryname, Image: sub.Image, Products: subProducts, }) } } for _, p := range products { if p.Subcategoryid == 0 { uncategorized = append(uncategorized, p) } } if len(uncategorized) > 0 { details = append(details, models.SubcategoryProductResponse{ SubcategoryID: 0, SubcategoryName: "Uncategorized", Products: uncategorized, }) } if params.TenantID > 0 { tenantInfo, err := s.repo.GetTenantInfo(params.TenantID, params.AppLocationID) if err == nil && tenantInfo != nil { tenantInfo["details"] = details return tenantInfo, nil } } return map[string]interface{}{"details": details}, nil } func (s *productService) UpdateProductLocation(input models.Productlocations) error { return s.repo.UpdateProductLocation(input) } func (s *productService) CreateProductLocation(input []models.Productlocations) error { return s.repo.CreateProductLocation(input) } func (s *productService) CreateProductVariant(input models.Productvariant) error { return s.repo.CreateProductVariant(input) } func (s *productService) DeleteProductLocation(tenantid, locationid, productid int) error { return s.repo.DeleteProductLocation(tenantid, locationid, productid) } // ImportCatalogueProduct bridges a global catalogue product (CatalogueDB) into // a tenant's own store catalogue: it snapshots the catalogue product into the // tenant's `products` table on first import (keyed on brand+catalogueid so // re-imports are recognized), then links it to the location via the existing // CreateProductLocation upsert, which already handles conflicting // (tenantid, locationid, productid) rows and stock-ledger entries. func (s *productService) ImportCatalogueProduct(reqs []models.ImportCatalogueProductRequest) error { locations := make([]models.Productlocations, 0, len(reqs)) for _, req := range reqs { catalogueProduct, err := s.catalogueService.GetProductByID(req.Brand, req.Catalogueid) if err != nil { return err } if catalogueProduct == nil { return fmt.Errorf("catalogue product not found: brand=%s id=%d", req.Brand, req.Catalogueid) } existing, err := s.repo.FindTenantProductByCatalogueRef(req.Tenantid, req.Brand, req.Catalogueid) if err != nil { return err } productID := 0 if existing != nil { productID = existing.Productid if err := s.repo.UpdateProductPricing(productID, req.Retailprice, req.Productcost, req.Taxpercent); err != nil { return err } } else { snapshot := models.Products{ Tenantid: req.Tenantid, Categoryid: req.Categoryid, Subcategoryid: req.Subcategoryid, Productname: catalogueProduct.ProductName, Productdesc: catalogueProduct.Description, Productsku: catalogueProduct.ProductSKU, Productbrand: catalogueProduct.Brand, Catalogueid: int(catalogueProduct.ID), Productunit: catalogueProduct.Size, Productcost: req.Productcost, Retailprice: req.Retailprice, Taxpercent: req.Taxpercent, Approve: 1, } if len(catalogueProduct.Images) > 0 { snapshot.Productimage = catalogueProduct.Images[0] } productID, err = s.repo.CreateProductReturningID(snapshot) if err != nil { return err } } locations = append(locations, models.Productlocations{ Tenantid: req.Tenantid, Locationid: req.Locationid, Productid: productID, Quantity: req.Quantity, Stocktype: req.Stocktype, Status: req.Status, }) } return s.repo.CreateProductLocation(locations) } func (s *productService) GetImportedCatalogueRefs(tenantid int, brand string) ([]models.ImportedCatalogueRef, error) { return s.repo.GetImportedCatalogueRefs(tenantid, brand) } func (s *productService) GetTenantCategories(tenantid int) ([]models.TenantCategory, error) { return s.repo.GetTenantCategories(tenantid) }