fix on stocks on store catalogue

This commit is contained in:
2026-08-31 16:26:06 +05:30
parent d55f101834
commit bc10b589cd
4 changed files with 172 additions and 11 deletions

View File

@@ -415,14 +415,21 @@ func (s *productService) ImportCatalogueProduct(reqs []models.ImportCataloguePro
// column — not products.retailprice — is what the store catalogue, the
// customer app and each order line read. The result was a catalogue
// where nothing had a price and every order booked an amount of 0.
// `publishedat` is deliberately absent — the field is `<-:false`, so
// this row is created unpublished and belongs to the admin catalogue
// only. Releasing it to the shops is PublishProduct, which refuses
// without a price.
// `publishedat` is not written HERE — the field is `<-:false` — but the
// row does not stay unpublished. PublishPricedLocations below releases
// the ones that arrived with a price, which is every row of a
// spreadsheet import.
//
// Importing used to be publication: this row existed, and membership of
// this table was the whole test, so a product reached every shop the
// moment it was imported and before anyone had priced it.
// The gate exists to stop an UNPRICED product reaching a till that would
// ring it up at zero. An import that carries a price has already met that
// condition, so demanding a second manual release protected nothing and
// cost a great deal: nothing in the backend filters on `publishedat`, so
// the products were on sale in the customer app while the merchant's own
// store catalogue — the one screen that does read it — showed nothing.
//
// Importing used to be publication unconditionally, which was the
// opposite error: a product reached every shop the moment it was
// imported and before anyone had priced it.
locations = append(locations, models.Productlocations{
Tenantid: req.Tenantid,
Locationid: req.Locationid,
@@ -434,7 +441,24 @@ func (s *productService) ImportCatalogueProduct(reqs []models.ImportCataloguePro
})
}
return s.repo.CreateProductLocation(locations)
if err := s.repo.CreateProductLocation(locations); err != nil {
return err
}
// Release what arrived priced.
//
// After the rows exist, not before: publishing addresses
// (tenantid, locationid, productid), and the productid of a brand-new
// snapshot is only known once it has been written.
refs := make([]models.ProductLocationRef, 0, len(locations))
for _, location := range locations {
refs = append(refs, models.ProductLocationRef{
Tenantid: location.Tenantid,
Locationid: location.Locationid,
Productid: location.Productid,
})
}
return s.repo.PublishPricedLocations(refs)
}
func (s *productService) GetImportedCatalogueRefs(tenantid int, brand string) ([]models.ImportedCatalogueRef, error) {

View File

@@ -42,9 +42,11 @@ type fakeProductRepo struct {
existingByImage *models.Products
linked [][2]any
ensuredRefs []models.ProductLocationRef
syncedRefs []models.ProductLocationRef
categorySet map[int][2]int // productid -> {categoryid, subcategoryid}
ensuredRefs []models.ProductLocationRef
syncedRefs []models.ProductLocationRef
publishedRefs []models.ProductLocationRef
created []models.Products
categorySet map[int][2]int // productid -> {categoryid, subcategoryid}
}
func newFakeRepo() *fakeProductRepo {
@@ -107,6 +109,26 @@ func (f *fakeProductRepo) UpdateProductCategory(productid, categoryid, subcatego
return nil
}
// Publishing what an import brought in priced.
//
// The gate exists to stop an UNPRICED product reaching a till; a spreadsheet
// import carries the price, so demanding a second manual release protected
// nothing — and cost the merchant sight of their own stock, because nothing in
// the backend filters on `publishedat` but the store catalogue screen does.
// A fresh import creates the snapshot; the existing cases all take the
// re-import branch, so this only became reachable when publishing did.
func (f *fakeProductRepo) CreateProductReturningID(product models.Products) (int, error) {
f.calls = append(f.calls, "CreateProductReturningID")
f.created = append(f.created, product)
return 9001, nil
}
func (f *fakeProductRepo) PublishPricedLocations(refs []models.ProductLocationRef) error {
f.calls = append(f.calls, "PublishPricedLocations")
f.publishedRefs = append(f.publishedRefs, refs...)
return nil
}
func (f *fakeProductRepo) CreateProductLocation(input []models.Productlocations) error {
f.calls = append(f.calls, "CreateProductLocation")
return nil
@@ -513,3 +535,74 @@ func TestBrowseStillHidesWhatTheShopDoesNotHave(t *testing.T) {
t.Fatalf("browse should still offer only the stocked product, got %v", names(offered))
}
}
/*
A priced import releases itself.
The publish gate is there to stop an unpriced product reaching a till that would
ring it up at zero. A spreadsheet import carries the price on every row, so the
condition is already met at import time and a second manual release could only
ever answer "yes".
It was not a harmless extra click. Nothing in the backend filters on
`publishedat` — only the merchant's own store-catalogue screen does — so on
2026-08-31 seventeen products at R mart were on sale in the customer app, priced
and stocked, while the shopkeeper's store catalogue showed none of them. The one
person who could not see the stock was the one selling it.
*/
func TestImportPublishesWhatItPriced(t *testing.T) {
repo := newFakeRepo()
service := NewProductService(repo, &fakeCatalogueService{
product: &models.CatalogueProduct{
ID: 1, Brand: "kohinoor", ProductName: "Kohinoor Charminar Rice 5kg",
ImageID: "kohinoor_kohinoor_charminar_rice_5kg",
},
})
if err := service.ImportCatalogueProduct([]models.ImportCatalogueProductRequest{{
Tenantid: 1147, Locationid: 1185, Brand: "kohinoor", Catalogueid: 1,
Categoryid: 2, Retailprice: 395, Quantity: 22, Stocktype: "in",
}}); err != nil {
t.Fatalf("ImportCatalogueProduct: %v", err)
}
if len(repo.publishedRefs) != 1 {
t.Fatalf("want the imported row released, got %d refs (calls: %v)",
len(repo.publishedRefs), repo.calls)
}
got := repo.publishedRefs[0]
// The branch that was imported into, and only that one. Publishing
// tenant-wide would put products in outlets nobody chose.
if got.Tenantid != 1147 || got.Locationid != 1185 {
t.Errorf("released the wrong row: %+v", got)
}
}
// Order matters: publishing addresses (tenantid, locationid, productid), and a
// brand-new snapshot has no productid until its rows are written.
func TestImportPublishesAfterTheRowsExist(t *testing.T) {
repo := newFakeRepo()
service := NewProductService(repo, &fakeCatalogueService{
product: &models.CatalogueProduct{ID: 1, Brand: "colin", ProductName: "Colin Glass Cleaner 500ml"},
})
if err := service.ImportCatalogueProduct([]models.ImportCatalogueProductRequest{{
Tenantid: 1147, Locationid: 1185, Brand: "colin", Catalogueid: 1,
Categoryid: 2, Retailprice: 105, Quantity: 42,
}}); err != nil {
t.Fatalf("ImportCatalogueProduct: %v", err)
}
create, publish := -1, -1
for i, call := range repo.calls {
if call == "CreateProductLocation" {
create = i
}
if call == "PublishPricedLocations" {
publish = i
}
}
if create < 0 || publish < 0 || publish < create {
t.Fatalf("want CreateProductLocation before PublishPricedLocations, got %v", repo.calls)
}
}