console categories
This commit is contained in:
@@ -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.
|
// ResolveCategories exchanges category names for this tenant's category ids.
|
||||||
//
|
//
|
||||||
// POST, not GET, because it CREATES the categories it cannot find — a sheet
|
// POST, not GET, because it CREATES the categories it cannot find — a sheet
|
||||||
|
|||||||
@@ -21,6 +21,13 @@ type Productcount struct {
|
|||||||
Outofstock int `json:"outofstock"`
|
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 {
|
type ProductCategory struct {
|
||||||
Categoryid int `json:"categoryid"`
|
Categoryid int `json:"categoryid"`
|
||||||
Moduleid int `json:"moduleid"`
|
Moduleid int `json:"moduleid"`
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type ProductRepository interface {
|
|||||||
GetProductCount(tenantID, categoryID, subcategoryID int, approve string) ([]models.Productcount, error)
|
GetProductCount(tenantID, categoryID, subcategoryID int, approve string) ([]models.Productcount, error)
|
||||||
GetProductCategory() ([]models.ProductCategory, error)
|
GetProductCategory() ([]models.ProductCategory, error)
|
||||||
EnsureTenantCategories(tenantID int, names []string) (map[string]int, 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)
|
GetProductVariants(tenantID, subcategoryID int) ([]models.Productvariant, error)
|
||||||
GetCatalougeProducts(tenantID, locationID, subcategoryID, pageno, pagesize int, keyword string) ([]models.Products, error)
|
GetCatalougeProducts(tenantID, locationID, subcategoryID, pageno, pagesize int, keyword string) ([]models.Products, error)
|
||||||
GetProductStocks(tenantID, locationID string) ([]models.Productstocks, 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
|
// row despite being in real use). This is deliberately not the global
|
||||||
// productcategories list: that list can omit categoryids tenants actually
|
// productcategories list: that list can omit categoryids tenants actually
|
||||||
// have products in, which would make the import category picker unusable.
|
// 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,
|
// EnsureTenantCategories maps category NAMES to this tenant's category ids,
|
||||||
// creating any that do not exist yet, and is safe to call repeatedly.
|
// creating any that do not exist yet, and is safe to call repeatedly.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ func RegisterProductRoutes(api fiber.Router, f *facade.Facade) {
|
|||||||
|
|
||||||
products.Get("/gettenantcategories", f.ProductController.GetTenantCategories)
|
products.Get("/gettenantcategories", f.ProductController.GetTenantCategories)
|
||||||
products.Post("/resolvecategories", f.ProductController.ResolveCategories)
|
products.Post("/resolvecategories", f.ProductController.ResolveCategories)
|
||||||
|
products.Put("/recategorise", f.ProductController.Recategorise)
|
||||||
products.Delete("/deleteproductlocation", f.ProductController.DeleteProductLocation)
|
products.Delete("/deleteproductlocation", f.ProductController.DeleteProductLocation)
|
||||||
|
|
||||||
// Releasing a product to the shops. Importing no longer does this: a
|
// Releasing a product to the shops. Importing no longer does this: a
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ type ProductService interface {
|
|||||||
RelinkCatalogue(tenantid int, dryRun bool) (*RelinkReport, error)
|
RelinkCatalogue(tenantid int, dryRun bool) (*RelinkReport, error)
|
||||||
GetTenantCategories(tenantid int) ([]models.TenantCategory, error)
|
GetTenantCategories(tenantid int) ([]models.TenantCategory, error)
|
||||||
EnsureTenantCategories(tenantid int, names []string) (map[string]int, 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)
|
PublishProduct(tenantID, productID int, price, taxPercent float64) (int, error)
|
||||||
UnpublishProduct(tenantID, productID int) (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)
|
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.
|
// 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
|
// The price check lives in the repository rather than here, so it holds for any
|
||||||
|
|||||||
Reference in New Issue
Block a user