changes fixed in the catalogue

This commit is contained in:
2026-08-11 17:10:21 +05:30
parent fba42259ea
commit ff72af9a8a
4 changed files with 65 additions and 14 deletions

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"nearle/models" "nearle/models"
"nearle/services" "nearle/services"
@@ -573,10 +574,43 @@ func (ctl *ProductController) ImportCatalogueProduct(c *fiber.Ctx) error {
} }
for _, req := range data { for _, req := range data {
if req.Tenantid == 0 || req.Locationid == 0 || req.Brand == "" || req.Catalogueid == 0 || req.Categoryid == 0 { // `categoryid` is deliberately NOT required.
//
// Importing adds a product to the admin catalogue and nothing else — it
// reaches no shop and cannot be sold — so there is nothing yet that
// depends on it being classified. The console matches the tenant's own
// category against the catalogue's where one lines up and sends 0 where
// none does, and the admin corrects it there.
//
// Requiring it forced the console to send *something*, and what it sent
// was the tenant's first category regardless of the product. That is
// worse than uncategorised: an unclassified product is visibly
// unfinished, while a wrongly classified one looks done and is only
// found by someone browsing the wrong aisle.
// Named individually rather than as one list of five.
//
// The old message recited every required field whichever one was
// actually missing, so a request rejected for `categoryid: 0` read as
// "catalogueid is required" and sent somebody looking at the wrong
// field entirely. An error that does not say what is wrong costs more
// than the branch it saves.
var missing []string
if req.Tenantid == 0 {
missing = append(missing, "tenantid")
}
if req.Locationid == 0 {
missing = append(missing, "locationid")
}
if req.Brand == "" {
missing = append(missing, "brand")
}
if req.Catalogueid == 0 {
missing = append(missing, "catalogueid")
}
if len(missing) > 0 {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{ return c.Status(http.StatusBadRequest).JSON(fiber.Map{
"code": http.StatusBadRequest, "code": http.StatusBadRequest,
"message": "tenantid, locationid, brand, catalogueid, and categoryid are required", "message": fmt.Sprintf("missing required field(s): %s", strings.Join(missing, ", ")),
"status": false, "status": false,
}) })
} }
@@ -703,6 +737,10 @@ func (ctl *ProductController) PublishProduct(c *fiber.Ctx) error {
Tenantid int `json:"tenantid"` Tenantid int `json:"tenantid"`
Productid int `json:"productid"` Productid int `json:"productid"`
Price float64 `json:"price"` Price float64 `json:"price"`
// Optional. Omitted leaves the product at 0%, which is a real rate for
// staples rather than a missing value — so it is written as given
// rather than skipped when zero.
Taxpercent float64 `json:"taxpercent"`
} }
if err := c.BodyParser(&input); err != nil { if err := c.BodyParser(&input); err != nil {
@@ -711,7 +749,7 @@ func (ctl *ProductController) PublishProduct(c *fiber.Ctx) error {
}) })
} }
outlets, err := ctl.productService.PublishProduct(input.Tenantid, input.Productid, input.Price) outlets, err := ctl.productService.PublishProduct(input.Tenantid, input.Productid, input.Price, input.Taxpercent)
if err != nil { if err != nil {
// 400 rather than 500: every failure here is something the caller can // 400 rather than 500: every failure here is something the caller can
// act on — no price, no outlets, or a product that is not theirs. // act on — no price, no outlets, or a product that is not theirs.

View File

@@ -30,13 +30,20 @@ import (
// Idempotent: publishing an already-published product re-prices it and leaves // Idempotent: publishing an already-published product re-prices it and leaves
// the original `publishedat` alone, so "when did this go live" survives a // the original `publishedat` alone, so "when did this go live" survives a
// second click. // second click.
func (r *productRepository) PublishProduct(tenantID, productID int, price float64) (int, error) { func (r *productRepository) PublishProduct(tenantID, productID int, price, taxPercent float64) (int, error) {
if tenantID <= 0 || productID <= 0 { if tenantID <= 0 || productID <= 0 {
return 0, fmt.Errorf("tenantid and productid are required") return 0, fmt.Errorf("tenantid and productid are required")
} }
if price <= 0 { if price <= 0 {
return 0, fmt.Errorf("set a price before publishing — a product with no price cannot be sold at a till") return 0, fmt.Errorf("set a price before publishing — a product with no price cannot be sold at a till")
} }
// Zero is a legitimate rate — plenty of staples are zero-rated — so it
// cannot double as "not specified". Negative is refused rather than stored:
// three products already carry taxpercent -1, which yields negative tax
// under either convention.
if taxPercent < 0 {
return 0, fmt.Errorf("tax percentage cannot be negative")
}
var affected int var affected int
err := r.db.Transaction(func(tx *gorm.DB) error { err := r.db.Transaction(func(tx *gorm.DB) error {
@@ -93,9 +100,9 @@ func (r *productRepository) PublishProduct(tenantID, productID int, price float6
// The master price follows, so an outlet that has no row of its own // The master price follows, so an outlet that has no row of its own
// still reads the right figure through the COALESCE fallback in // still reads the right figure through the COALESCE fallback in
// GetProducts. // GetProducts.
return tx.Exec(`UPDATE products SET retailprice = ?, updated = NOW() return tx.Exec(`UPDATE products SET retailprice = ?, taxpercent = ?, updated = NOW()
WHERE productid = ? AND tenantid = ?`, WHERE productid = ? AND tenantid = ?`,
price, productID, tenantID).Error price, taxPercent, productID, tenantID).Error
}) })
return affected, err return affected, err

View File

@@ -42,7 +42,7 @@ type ProductRepository interface {
// Releasing a product from the admin catalogue to every outlet, and pulling // Releasing a product from the admin catalogue to every outlet, and pulling
// it back. See productPublishRepository.go. // it back. See productPublishRepository.go.
PublishProduct(tenantID, productID int, price float64) (int, error) PublishProduct(tenantID, productID int, price, taxPercent float64) (int, error)
UnpublishProduct(tenantID, productID int) (int, error) UnpublishProduct(tenantID, productID int) (int, error)
FindTenantProductByCatalogueRef(tenantid int, brand string, catalogueid int64) (*models.Products, error) FindTenantProductByCatalogueRef(tenantid int, brand string, catalogueid int64) (*models.Products, error)
CreateProductReturningID(product models.Products) (int, error) CreateProductReturningID(product models.Products) (int, error)
@@ -400,7 +400,7 @@ func (r *productRepository) GetStockStatement(tenantID, locationID, subcategoryI
// arithmetically identical to closing — the Inventory ledger then showed // arithmetically identical to closing — the Inventory ledger then showed
// the same number in both columns and looked like stock never moved, even // the same number in both columns and looked like stock never moved, even
// on days with sales. // on days with sales.
query := `SELECT a.productid,a.productname,a.productimage,a.categoryid,a.subcategoryid,a.productunit,a.unitvalue,a.productcost,a.taxpercent,a.taxamount,a.retailprice,b.tenantid,b.locationid,b.publishedat, query := `SELECT a.productid,a.productname,a.productimage,a.categoryid,a.subcategoryid,a.productunit,a.unitvalue,a.productcost,a.taxpercent,a.taxamount,a.retailprice,b.tenantid,b.locationid,
COALESCE( SUM(CASE WHEN UPPER(c.stocktype) = 'IN' AND c.stockdate::date < CURRENT_DATE THEN c.quantity ELSE 0 END) - COALESCE( SUM(CASE WHEN UPPER(c.stocktype) = 'IN' AND c.stockdate::date < CURRENT_DATE THEN c.quantity ELSE 0 END) -
SUM(CASE WHEN UPPER(c.stocktype) = 'OUT' AND c.stockdate::date < CURRENT_DATE THEN c.quantity ELSE 0 END),0 ) SUM(CASE WHEN UPPER(c.stocktype) = 'OUT' AND c.stockdate::date < CURRENT_DATE THEN c.quantity ELSE 0 END),0 )
AS opening, AS opening,
@@ -435,7 +435,7 @@ func (r *productRepository) GetStockStatement(tenantID, locationID, subcategoryI
a.productid, a.productname, a.productimage, a.productid, a.productname, a.productimage,
a.categoryid, a.subcategoryid, a.productunit, a.categoryid, a.subcategoryid, a.productunit,
a.productcost, a.taxpercent, a.taxamount, a.productcost, a.taxpercent, a.taxamount,
a.retailprice, b.tenantid, b.locationid, b.publishedat a.retailprice, b.tenantid, b.locationid
ORDER BY a.productid DESC LIMIT ` + strconv.Itoa(pagesize) + ` OFFSET ` + strconv.Itoa(offset) ORDER BY a.productid DESC LIMIT ` + strconv.Itoa(pagesize) + ` OFFSET ` + strconv.Itoa(offset)
if err := r.db.Raw(query, params...).Scan(&data).Error; err != nil { if err := r.db.Raw(query, params...).Scan(&data).Error; err != nil {
@@ -475,7 +475,13 @@ func (r *productRepository) GetLocationProducts(tenantID, locationID, subcategor
// retailprice when the store hasn't set its own. Returning a bare b.price // retailprice when the store hasn't set its own. Returning a bare b.price
// reported 0 for any product priced only at tenant level, which the store // reported 0 for any product priced only at tenant level, which the store
// catalogue then rendered as "—". // catalogue then rendered as "—".
query := `SELECT a.*, b.productlocationid, b.status, // b.publishedat decides whether a store user may see this row at all, so it
// has to be selected explicitly for the same reason b.price does: `a.*`
// covers products, and publication is a per-outlet fact on
// productlocations. Without it the column came back null for every row —
// including ones that had just been published — and the store catalogue,
// which filters on it, showed nothing.
query := `SELECT a.*, b.productlocationid, b.status, b.publishedat,
COALESCE(NULLIF(b.price, 0), a.retailprice, 0) AS price, COALESCE(NULLIF(b.price, 0), a.retailprice, 0) AS price,
COALESCE(SUM(CASE WHEN UPPER(c.stocktype) = 'IN' THEN c.quantity ELSE 0 END), 0) AS total_in, COALESCE(SUM(CASE WHEN UPPER(c.stocktype) = 'IN' THEN c.quantity ELSE 0 END), 0) AS total_in,
COALESCE(SUM(CASE WHEN UPPER(c.stocktype) = 'OUT' THEN c.quantity ELSE 0 END), 0) AS total_out, COALESCE(SUM(CASE WHEN UPPER(c.stocktype) = 'OUT' THEN c.quantity ELSE 0 END), 0) AS total_out,
@@ -500,7 +506,7 @@ func (r *productRepository) GetLocationProducts(tenantID, locationID, subcategor
query += ` GROUP BY a.productid, a.productname, a.productimage, a.categoryid, a.subcategoryid, query += ` GROUP BY a.productid, a.productname, a.productimage, a.categoryid, a.subcategoryid,
a.productunit, a.productcost, a.taxpercent, a.taxamount, a.retailprice, a.productunit, a.productcost, a.taxpercent, a.taxamount, a.retailprice,
b.tenantid, b.locationid, b.productlocationid, b.status, b.price b.tenantid, b.locationid, b.productlocationid, b.status, b.price, b.publishedat
ORDER BY a.productid DESC LIMIT ? OFFSET ?` ORDER BY a.productid DESC LIMIT ? OFFSET ?`
params = append(params, pagesize, offset) params = append(params, pagesize, offset)

View File

@@ -35,7 +35,7 @@ type ProductService interface {
ImportCatalogueProduct(reqs []models.ImportCatalogueProductRequest) error ImportCatalogueProduct(reqs []models.ImportCatalogueProductRequest) error
GetImportedCatalogueRefs(tenantid int, brand string) ([]models.ImportedCatalogueRef, error) GetImportedCatalogueRefs(tenantid int, brand string) ([]models.ImportedCatalogueRef, error)
GetTenantCategories(tenantid int) ([]models.TenantCategory, error) GetTenantCategories(tenantid int) ([]models.TenantCategory, error)
PublishProduct(tenantID, productID int, price float64) (int, error) PublishProduct(tenantID, productID int, price, taxPercent float64) (int, error)
UnpublishProduct(tenantID, productID int) (int, error) UnpublishProduct(tenantID, productID int) (int, error)
} }
type productService struct { type productService struct {
@@ -352,8 +352,8 @@ func (s *productService) GetTenantCategories(tenantid int) ([]models.TenantCateg
// //
// 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
// caller — the console form is a convenience, not the rule. // caller — the console form is a convenience, not the rule.
func (s *productService) PublishProduct(tenantID, productID int, price float64) (int, error) { func (s *productService) PublishProduct(tenantID, productID int, price, taxPercent float64) (int, error) {
return s.repo.PublishProduct(tenantID, productID, price) return s.repo.PublishProduct(tenantID, productID, price, taxPercent)
} }
func (s *productService) UnpublishProduct(tenantID, productID int) (int, error) { func (s *productService) UnpublishProduct(tenantID, productID int) (int, error) {