Files
backend_fiesta/services/productService.go
Suriya fab06bb33e Bridge global catalogue DB to per-store product catalogue
Adds a separate CatalogueDB (pgvector) connection alongside the main
nearledb, plus a new catalogue module (repository/service/controller/
routes) to browse it by brand, category, and keyword, with brand
optional so the whole ~237-product catalogue can be browsed unfiltered.

Adds the actual bridge: importing a catalogue product snapshots it into
the tenant's own products table (keyed on brand+catalogueid, since a
catalogue row's bare id is only unique within its own brand table),
then links it via the existing productlocations upsert. Re-importing
tops up stock and refreshes price instead of duplicating. Also adds an
imported-refs endpoint so the frontend can badge already-imported items
without diffing full product lists, and wires the new AWS S3 image
store used to resolve catalogue product photos.

Bumps Go/Docker to 1.24 for the AWS SDK dependency this needs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 17:34:20 +05:30

291 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)
}
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)
}