console categories

This commit is contained in:
2026-09-07 17:53:35 +05:30
parent c699a400c3
commit e87f09c4f2
5 changed files with 107 additions and 0 deletions

View File

@@ -787,6 +787,48 @@ func (ctl *ProductController) GetTenantCategories(c *fiber.Ctx) error {
})
}
// Recategorise re-files a tenant's products in bulk.
//
// PUT and not POST: it changes rows that already exist and creates none. The
// response carries how many rows actually moved, which is the only way a caller
// can tell "all done" from "every id I sent was wrong".
func (ctl *ProductController) Recategorise(c *fiber.Ctx) error {
var body struct {
Tenantid int `json:"tenantid"`
Updates []models.ProductCategoryUpdate `json:"updates"`
}
if err := c.BodyParser(&body); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
"code": http.StatusBadRequest,
"message": "invalid body",
"status": false,
})
}
if body.Tenantid == 0 {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
"code": http.StatusBadRequest,
"message": "tenantid is required",
"status": false,
})
}
moved, err := ctl.productService.RecategoriseProducts(body.Tenantid, body.Updates)
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
"code": http.StatusInternalServerError,
"message": err.Error(),
"status": false,
})
}
return c.JSON(fiber.Map{
"code": http.StatusOK,
"message": "Success",
"status": true,
"details": fiber.Map{"moved": moved},
})
}
// ResolveCategories exchanges category names for this tenant's category ids.
//
// POST, not GET, because it CREATES the categories it cannot find — a sheet

View File

@@ -21,6 +21,13 @@ type Productcount struct {
Outofstock int `json:"outofstock"`
}
// ProductCategoryUpdate is one row of a bulk re-filing: which product, and
// which category it should sit in.
type ProductCategoryUpdate struct {
Productid int `json:"productid"`
Categoryid int `json:"categoryid"`
}
type ProductCategory struct {
Categoryid int `json:"categoryid"`
Moduleid int `json:"moduleid"`

View File

@@ -18,6 +18,7 @@ type ProductRepository interface {
GetProductCount(tenantID, categoryID, subcategoryID int, approve string) ([]models.Productcount, error)
GetProductCategory() ([]models.ProductCategory, error)
EnsureTenantCategories(tenantID int, names []string) (map[string]int, error)
RecategoriseProducts(tenantID int, updates []models.ProductCategoryUpdate) (int64, 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)
@@ -1404,6 +1405,56 @@ func (r *productRepository) GetImportedCatalogueRefs(tenantid int, brand string)
// row despite being in real use). This is deliberately not the global
// productcategories list: that list can omit categoryids tenants actually
// have products in, which would make the import category picker unusable.
// RecategoriseProducts moves products into different categories, in bulk.
//
// Written for the backfill that re-files everything imported before categories
// existed: those rows all carry whichever single category the tenant had, while
// the catalogue has known all along what each product actually is.
//
// ── Two things this deliberately does not do ────────────────────────────────
//
// It does not touch any column but categoryid. UpdateProduct next door runs
// GORM's Updates over a whole struct, which is why it can only be trusted with
// a hand-built partial — a bulk caller sending full rows would write back every
// field it happened to have read, including a price it never meant to change.
// One column, named explicitly, cannot do that.
//
// It is scoped by tenantid in the WHERE clause, not just trusted from the
// caller. A productid is global, so a mistyped id in a list would otherwise
// re-file another merchant's product into a category that does not exist for
// them — invisible to both shops and to the app.
func (r *productRepository) RecategoriseProducts(tenantID int, updates []models.ProductCategoryUpdate) (int64, error) {
if tenantID == 0 || len(updates) == 0 {
return 0, nil
}
placeholders := make([]string, 0, len(updates))
args := make([]any, 0, len(updates)*2)
for _, u := range updates {
if u.Productid == 0 || u.Categoryid == 0 {
// categoryid 0 is the value the customer app rejects outright.
// Refusing it here means a bad row is skipped rather than hiding a
// product from every shopper.
continue
}
placeholders = append(placeholders, "(?::bigint, ?::bigint)")
args = append(args, u.Productid, u.Categoryid)
}
if len(placeholders) == 0 {
return 0, nil
}
args = append(args, tenantID)
tx := r.db.Exec(`
UPDATE products p
SET categoryid = v.categoryid
FROM (VALUES `+strings.Join(placeholders, ",")+`) AS v(productid, categoryid)
WHERE p.productid = v.productid
AND p.tenantid = ?
`, args...)
return tx.RowsAffected, tx.Error
}
// EnsureTenantCategories maps category NAMES to this tenant's category ids,
// creating any that do not exist yet, and is safe to call repeatedly.
//

View File

@@ -37,6 +37,7 @@ func RegisterProductRoutes(api fiber.Router, f *facade.Facade) {
products.Get("/gettenantcategories", f.ProductController.GetTenantCategories)
products.Post("/resolvecategories", f.ProductController.ResolveCategories)
products.Put("/recategorise", f.ProductController.Recategorise)
products.Delete("/deleteproductlocation", f.ProductController.DeleteProductLocation)
// Releasing a product to the shops. Importing no longer does this: a

View File

@@ -41,6 +41,7 @@ type ProductService interface {
RelinkCatalogue(tenantid int, dryRun bool) (*RelinkReport, error)
GetTenantCategories(tenantid int) ([]models.TenantCategory, error)
EnsureTenantCategories(tenantid int, names []string) (map[string]int, error)
RecategoriseProducts(tenantid int, updates []models.ProductCategoryUpdate) (int64, error)
PublishProduct(tenantID, productID int, price, taxPercent float64) (int, error)
UnpublishProduct(tenantID, productID int) (int, error)
}
@@ -511,6 +512,11 @@ func (s *productService) EnsureTenantCategories(tenantid int, names []string) (m
return s.repo.EnsureTenantCategories(tenantid, names)
}
// RecategoriseProducts moves a tenant's products into different categories.
func (s *productService) RecategoriseProducts(tenantid int, updates []models.ProductCategoryUpdate) (int64, error) {
return s.repo.RecategoriseProducts(tenantid, updates)
}
// PublishProduct releases a product to every outlet the tenant runs.
//
// The price check lives in the repository rather than here, so it holds for any