fix in the product import

This commit is contained in:
2026-08-11 15:49:04 +05:30
parent c179e4d642
commit fba42259ea
7 changed files with 259 additions and 2 deletions

View File

@@ -0,0 +1,119 @@
package repositories
import (
"fmt"
"gorm.io/gorm"
)
// Releasing a product from the admin catalogue to the shops.
//
// Until this existed there was no such step. The import created a
// `productlocations` row and membership of that table *was* publication, so a
// product reached every store the moment it was imported — unpriced, because
// pricing had not happened yet. That is how the platform ended up with a
// catalogue of products a till could not ring up.
//
// Two rules, both enforced here rather than in the console, because a rule that
// only exists in a form is a rule that holds until somebody calls the API:
//
// 1. **A product cannot be published without a price.** An unpriced product
// reaching a shop is the exact failure this separation prevents — the POS
// catalogue sends it down as `is_active: false` and it cannot be sold.
// 2. **Publishing covers every outlet the tenant runs.** A price is a
// business-level decision, and a product live at one branch and absent from
// another is a support call nobody can explain.
// PublishProduct releases one product to every outlet of a tenant at the given
// price, creating the store link where it does not exist yet.
//
// 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) {
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")
}
var affected int
err := r.db.Transaction(func(tx *gorm.DB) error {
// The tenant's outlets are read here rather than accepted from the
// caller. A console that sent its own list could publish to a subset by
// omission and nobody would notice which shop was missing.
var locationIDs []int
if err := tx.Raw(`
SELECT locationid FROM tenantlocations
WHERE tenantid = ? AND LOWER(COALESCE(status,'active')) <> 'inactive'
ORDER BY locationid`, tenantID).Scan(&locationIDs).Error; err != nil {
return err
}
if len(locationIDs) == 0 {
return fmt.Errorf("this business has no active outlet to publish to")
}
// Verify the product belongs to this tenant before writing anything
// against it. Everything below is keyed on (tenantid, productid), but a
// caller naming another tenant's product would otherwise create rows for
// a product that is not theirs.
var owned int64
if err := tx.Raw(`SELECT COUNT(1) FROM products WHERE productid = ? AND tenantid = ?`,
productID, tenantID).Scan(&owned).Error; err != nil {
return err
}
if owned == 0 {
return fmt.Errorf("product %d does not belong to this business", productID)
}
for _, locationID := range locationIDs {
// COALESCE on publishedat keeps the first release date through a
// re-publish; NOW() only applies to a row that has never been live.
res := tx.Exec(`
UPDATE productlocations
SET price = ?, publishedat = COALESCE(publishedat, NOW()), updated = NOW()
WHERE tenantid = ? AND locationid = ? AND productid = ?`,
price, tenantID, locationID, productID)
if res.Error != nil {
return res.Error
}
if res.RowsAffected == 0 {
if err := tx.Exec(`
INSERT INTO productlocations
(tenantid, locationid, productid, price, status, publishedat, created, updated)
VALUES (?, ?, ?, ?, 'Active', NOW(), NOW(), NOW())`,
tenantID, locationID, productID, price).Error; err != nil {
return err
}
}
affected++
}
// 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()
WHERE productid = ? AND tenantid = ?`,
price, productID, tenantID).Error
})
return affected, err
}
// UnpublishProduct withdraws a product from every shop.
//
// Clears `publishedat` and keeps the rows. Deleting them would lose the price,
// the stock ledger's link and any history, and a product pulled from sale for a
// week should come back the way it left.
func (r *productRepository) UnpublishProduct(tenantID, productID int) (int, error) {
if tenantID <= 0 || productID <= 0 {
return 0, fmt.Errorf("tenantid and productid are required")
}
res := r.db.Exec(`
UPDATE productlocations SET publishedat = NULL, updated = NOW()
WHERE tenantid = ? AND productid = ? AND publishedat IS NOT NULL`,
tenantID, productID)
return int(res.RowsAffected), res.Error
}

View File

@@ -39,6 +39,11 @@ type ProductRepository interface {
CreateProductLocation(input []models.Productlocations) error
CreateProductVariant(input models.Productvariant) error
DeleteProductLocation(tenantid, locationid, productid int) error
// 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)
UnpublishProduct(tenantID, productID int) (int, error)
FindTenantProductByCatalogueRef(tenantid int, brand string, catalogueid int64) (*models.Products, error)
CreateProductReturningID(product models.Products) (int, error)
GetImportedCatalogueRefs(tenantid int, brand string) ([]models.ImportedCatalogueRef, error)
@@ -395,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,
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,
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,
@@ -430,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
a.retailprice, b.tenantid, b.locationid, b.publishedat
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 {