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>
296 lines
10 KiB
Go
296 lines
10 KiB
Go
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)
|
|
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)
|
|
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
|
|
}
|
|
|
|
idMap := make(map[int]struct{})
|
|
var productIDs []int
|
|
for _, s := range stocks {
|
|
if s.Productid > 0 {
|
|
if _, exists := idMap[s.Productid]; !exists {
|
|
idMap[s.Productid] = struct{}{}
|
|
productIDs = append(productIDs, s.Productid)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(productIDs) > 0 {
|
|
if err := s.repo.UpdateProductStatus(productIDs, "available"); 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) 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 int) ([]models.Products, error) {
|
|
|
|
var data []models.Products
|
|
|
|
result, err := s.repo.GetProductByVariant(tenantid, variantid)
|
|
|
|
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)
|
|
}
|
|
|