317 lines
12 KiB
Go
317 lines
12 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)
|
|
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)
|
|
}
|
|
|