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>
This commit is contained in:
42
services/catalogueService.go
Normal file
42
services/catalogueService.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"nearle/models"
|
||||
"nearle/repositories"
|
||||
)
|
||||
|
||||
type CatalogueService interface {
|
||||
GetBrands() ([]models.CatalogueBrand, error)
|
||||
GetCategories(brand string) ([]string, error)
|
||||
GetProducts(brand, category, keyword string, pageno, pagesize int) ([]models.CatalogueProduct, int64, error)
|
||||
GetProductBySKU(brand, sku string) (*models.CatalogueProduct, error)
|
||||
GetProductByID(brand string, id int64) (*models.CatalogueProduct, error)
|
||||
}
|
||||
|
||||
type catalogueService struct {
|
||||
repo repositories.CatalogueRepository
|
||||
}
|
||||
|
||||
func NewCatalogueService(repo repositories.CatalogueRepository) CatalogueService {
|
||||
return &catalogueService{repo: repo}
|
||||
}
|
||||
|
||||
func (s *catalogueService) GetBrands() ([]models.CatalogueBrand, error) {
|
||||
return s.repo.GetBrands()
|
||||
}
|
||||
|
||||
func (s *catalogueService) GetCategories(brand string) ([]string, error) {
|
||||
return s.repo.GetCategories(brand)
|
||||
}
|
||||
|
||||
func (s *catalogueService) GetProducts(brand, category, keyword string, pageno, pagesize int) ([]models.CatalogueProduct, int64, error) {
|
||||
return s.repo.GetProducts(brand, category, keyword, pageno, pagesize)
|
||||
}
|
||||
|
||||
func (s *catalogueService) GetProductBySKU(brand, sku string) (*models.CatalogueProduct, error) {
|
||||
return s.repo.GetProductBySKU(brand, sku)
|
||||
}
|
||||
|
||||
func (s *catalogueService) GetProductByID(brand string, id int64) (*models.CatalogueProduct, error) {
|
||||
return s.repo.GetProductByID(brand, id)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"nearle/models"
|
||||
"nearle/repositories"
|
||||
"time"
|
||||
@@ -28,13 +29,16 @@ type ProductService interface {
|
||||
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
|
||||
repo repositories.ProductRepository
|
||||
catalogueService CatalogueService
|
||||
}
|
||||
|
||||
func NewProductService(repo repositories.ProductRepository) ProductService {
|
||||
return &productService{repo: repo}
|
||||
func NewProductService(repo repositories.ProductRepository, catalogueService CatalogueService) ProductService {
|
||||
return &productService{repo: repo, catalogueService: catalogueService}
|
||||
}
|
||||
|
||||
func (s *productService) GetProductSubCategory(categoryID, tenantID int) ([]models.ProductSubCategory, error) {
|
||||
@@ -212,3 +216,75 @@ func (s *productService) DeleteProductLocation(tenantid, locationid, productid i
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user