Two gaps found while auditing the order/stock flow: - Receiving stock via an approved stock request only updated products.productstatus (a global per-product flag). A location flagged outofstock by CreateOrder never got reactivated, since nothing touched productlocations.status on the way back in. CreateProductStock now reactivates the specific (tenant, location, product) row to "available" for every "in" entry, the counterpart to how it gets flagged out. - getproductbyvariant returned no stock info at all, so the app could only find out a product was unavailable from the 409 at order time. It now accepts an optional locationid and, when passed, returns live productstock (same SUM(in)-SUM(out) formula the order check uses) and locationstatus per product. Omitting locationid keeps the old response shape. Added MOBILE_ORDER_VERIFICATION.md as a handoff doc for the mobile team covering the expected request/response shapes and how to verify their integration. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
314 lines
11 KiB
Go
314 lines
11 KiB
Go
package services
|
|
|
|
import (
|
|
"fmt"
|
|
"nearle/models"
|
|
"nearle/repositories"
|
|
"strings"
|
|
"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, 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
|
|
}
|
|
|
|
idMap := make(map[int]struct{})
|
|
var productIDs []int
|
|
locMap := make(map[models.ProductLocationRef]struct{})
|
|
var locRefs []models.ProductLocationRef
|
|
for _, stk := range stocks {
|
|
if stk.Productid > 0 {
|
|
if _, exists := idMap[stk.Productid]; !exists {
|
|
idMap[stk.Productid] = struct{}{}
|
|
productIDs = append(productIDs, stk.Productid)
|
|
}
|
|
}
|
|
// Only "in" entries mean stock actually arrived — an "out" entry
|
|
// (a sale) should never flip a location back to available.
|
|
if stk.Productid > 0 && stk.Locationid > 0 && stk.Tenantid > 0 && strings.EqualFold(stk.Stocktype, "in") {
|
|
ref := models.ProductLocationRef{Tenantid: stk.Tenantid, Locationid: stk.Locationid, Productid: stk.Productid}
|
|
if _, exists := locMap[ref]; !exists {
|
|
locMap[ref] = struct{}{}
|
|
locRefs = append(locRefs, ref)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(productIDs) > 0 {
|
|
if err := s.repo.UpdateProductStatus(productIDs, "available"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if len(locRefs) > 0 {
|
|
if err := s.repo.ReactivateProductLocations(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) 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)
|
|
}
|
|
|