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"
"net/http"
"strconv"
"strings"
"nearle/models"
"nearle/services"
@@ -573,10 +574,43 @@ func (ctl *ProductController) ImportCatalogueProduct(c *fiber.Ctx) error {
}
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{
"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,
})
}
@@ -703,6 +737,10 @@ func (ctl *ProductController) PublishProduct(c *fiber.Ctx) error {
Tenantid int `json:"tenantid"`
Productid int `json:"productid"`
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 {
@@ -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 {
// 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.

View File

@@ -30,13 +30,20 @@ import (
// Idempotent: publishing an already-published product re-prices it and leaves
// the original `publishedat` alone, so "when did this go live" survives a
// 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 {
return 0, fmt.Errorf("tenantid and productid are required")
}
if price <= 0 {
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
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
// still reads the right figure through the COALESCE fallback in
// GetProducts.
return tx.Exec(`UPDATE products SET retailprice = ?, updated = NOW()
return tx.Exec(`UPDATE products SET retailprice = ?, taxpercent = ?, updated = NOW()
WHERE productid = ? AND tenantid = ?`,
price, productID, tenantID).Error
price, taxPercent, productID, tenantID).Error
})
return affected, err

View File

@@ -42,7 +42,7 @@ type ProductRepository interface {
// Releasing a product from the admin catalogue to every outlet, and pulling
// 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)
FindTenantProductByCatalogueRef(tenantid int, brand string, catalogueid int64) (*models.Products, 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
// the same number in both columns and looked like stock never moved, even
// 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) -
SUM(CASE WHEN UPPER(c.stocktype) = 'OUT' AND c.stockdate::date < CURRENT_DATE THEN c.quantity ELSE 0 END),0 )
AS opening,
@@ -435,7 +435,7 @@ func (r *productRepository) GetStockStatement(tenantID, locationID, subcategoryI
a.productid, a.productname, a.productimage,
a.categoryid, a.subcategoryid, a.productunit,
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)
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
// reported 0 for any product priced only at tenant level, which the store
// 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(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,
@@ -500,7 +506,7 @@ func (r *productRepository) GetLocationProducts(tenantID, locationID, subcategor
query += ` GROUP BY a.productid, a.productname, a.productimage, a.categoryid, a.subcategoryid,
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 ?`
params = append(params, pagesize, offset)

View File

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