changes fixed in the catalogue
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user