product on shelf

This commit is contained in:
2026-08-31 14:59:48 +05:30
parent ffc66462fc
commit 4bce5ac854
6 changed files with 103 additions and 0 deletions

View File

@@ -130,3 +130,30 @@ func (ctl *CatalogueUploadController) MarkShelved(c *fiber.Ctx) error {
}
return c.JSON(fiber.Map{"code": 200, "message": "Shelving recorded", "status": true})
}
// AttachSheet supplies the prices and opening stock for a receipt that has
// none, so its products can still be put on a shelf.
//
// Needed because the receipt and the sheet used to part company. The ingest
// service holds neither price nor stock — its catalogue is shared by every
// merchant — so both live only in the spreadsheet, and a receipt written before
// those rows were stored has no way to finish. Handing the file over again is
// the only way back, and this is where it lands.
func (ctl *CatalogueUploadController) AttachSheet(c *fiber.Ctx) error {
var input struct {
Batchid string `json:"batchid"`
Sheetrows string `json:"sheetrows"`
}
if err := c.BodyParser(&input); err != nil {
return c.JSON(fiber.Map{"code": http.StatusBadRequest, "message": "Invalid input", "status": false})
}
if strings.TrimSpace(input.Batchid) == "" || strings.TrimSpace(input.Sheetrows) == "" {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
"code": http.StatusBadRequest, "message": "batchid and sheetrows are both required", "status": false,
})
}
if err := ctl.service.AttachSheet(input.Batchid, input.Sheetrows); err != nil {
return c.JSON(fiber.Map{"code": http.StatusInternalServerError, "message": err.Error(), "status": false})
}
return c.JSON(fiber.Map{"code": 200, "message": "Sheet attached", "status": true})
}

13
main.go
View File

@@ -156,6 +156,19 @@ func main() {
ON catalogueuploads (tenantid, locationid, created DESC)`).Error; err != nil {
log.Println("⚠️ could not add catalogueuploads scope index:", err)
}
// The sheet's own prices and opening stock, kept so shelving can happen
// after the browser that uploaded it is gone.
//
// Added separately from the CREATE above because the table already exists in
// production without it. The ingest service holds none of this — its
// catalogue is shared by every merchant and carries no price and no stock —
// so before this column the join could only be made in the tab that did the
// upload, and that tab is normally long closed by the time their admin
// releases the drop and the run finishes.
if err := db.DB.Exec(
`ALTER TABLE catalogueuploads ADD COLUMN IF NOT EXISTS sheetrows jsonb`).Error; err != nil {
log.Fatal("could not add catalogueuploads.sheetrows:", err)
}
f := facade.NewFacade(db.DB, db.CatalogueDB)

View File

@@ -59,6 +59,24 @@ type CatalogueUpload struct {
// the service reports, so a drop that never runs still says how big it was.
Rowcount int `json:"rowcount" gorm:"column:rowcount"`
// The sheet itself, as the console parsed it: SKU, price and opening stock
// per row, as JSON.
//
// Stored because the ingest service cannot hold it and nothing else can.
// Their pipeline writes the GLOBAL catalogue, which every merchant shares
// and which therefore carries no price and no stock; both live only in the
// sheet. Shelving is the step that joins the two, and it used to be possible
// only in the browser tab that did the upload, because that tab was the only
// place the parsed rows existed.
//
// That failed in the ordinary case rather than a rare one. A drop waits for
// their admin to release it and the run then takes minutes, so by the time
// there is anything to shelve the tab is usually gone — and the products sit
// in the global catalogue, unpriced and unstocked, with no way left to
// finish. Keeping the rows here is what lets the Uploads page complete it
// days later.
Sheetrows string `json:"sheetrows" gorm:"column:sheetrows;type:jsonb"`
// ── Cached from the ingest service, by whoever last polled ──────────────
Laststatus string `json:"laststatus" gorm:"column:laststatus;default:pending"`

View File

@@ -15,6 +15,7 @@ type CatalogueUploadRepository interface {
List(tenantID, locationID, pageNo, pageSize int) ([]models.CatalogueUpload, error)
UpdateStatus(upload *models.CatalogueUpload) error
MarkShelved(batchID string, shelved, skipped int) error
AttachSheet(batchID, sheetRows string) error
}
type catalogueUploadRepository struct {
@@ -148,3 +149,29 @@ func (r *catalogueUploadRepository) MarkShelved(batchID string, shelved, skipped
"shelvedat": gorm.Expr("CURRENT_TIMESTAMP"),
}).Error
}
// AttachSheet stores the sheet's rows against a receipt that has none.
//
// The rescue path for a receipt filed before the rows were kept, and for one
// whose upload predates this column. The prices and opening stock exist only in
// the spreadsheet; if the receipt was written without them, the only way back is
// for somebody to hand the file over again.
//
// Deliberately not part of UpdateStatus. That call is made by any browser
// polling the ingest service and must never touch what the console owns; this
// one is a person supplying missing data, and the two should not be able to
// happen by accident in each other's place.
func (r *catalogueUploadRepository) AttachSheet(batchID, sheetRows string) error {
if strings.TrimSpace(batchID) == "" {
return errors.New("batchid is required")
}
if strings.TrimSpace(sheetRows) == "" {
// Empty would be stored as `[]` and read back as "this sheet had no
// rows", which is a different claim from "nobody has supplied them
// yet" — and the second is what makes the shelve button appear.
return errors.New("sheetrows is required")
}
return r.db.Model(&models.CatalogueUpload{}).
Where("batchid = ?", batchID).
Update("sheetrows", sheetRows).Error
}

View File

@@ -29,5 +29,9 @@ func RegisterUploadRoutes(api fiber.Router, f *facade.Facade) {
// The other half: priced, shelved and stocked at a branch. Kept apart from
// /update because the ingest service confirms the shared global catalogue
// and knows nothing about a shop's shelf.
// The rescue path: hand the spreadsheet over again for a receipt whose
// prices and opening stock were never stored, so it can still be shelved.
uploads.Put("/sheet", f.CatalogueUploadController.AttachSheet)
uploads.Put("/shelved", f.CatalogueUploadController.MarkShelved)
}

View File

@@ -1,6 +1,8 @@
package services
import (
"strings"
"nearle/models"
"nearle/repositories"
)
@@ -23,6 +25,7 @@ type CatalogueUploadService interface {
List(tenantID, locationID, pageNo, pageSize int) ([]models.CatalogueUpload, error)
UpdateStatus(upload *models.CatalogueUpload) error
MarkShelved(batchID string, shelved, skipped int) error
AttachSheet(batchID, sheetRows string) error
}
type catalogueUploadService struct {
@@ -40,6 +43,13 @@ func (s *catalogueUploadService) Record(upload *models.CatalogueUpload) error {
// the pipeline had it when it is in fact waiting for a person.
upload.Laststatus = "pending"
}
if strings.TrimSpace(upload.Sheetrows) == "" {
// `sheetrows` is jsonb, and Postgres rejects an empty string for it —
// "invalid input syntax for type json" — which would fail the whole
// receipt over a field nobody supplied. An empty ARRAY is the honest
// value: no rows were sent, so there is nothing to shelve from later.
upload.Sheetrows = "[]"
}
return s.repo.Record(upload)
}
@@ -54,3 +64,7 @@ func (s *catalogueUploadService) UpdateStatus(upload *models.CatalogueUpload) er
func (s *catalogueUploadService) MarkShelved(batchID string, shelved, skipped int) error {
return s.repo.MarkShelved(batchID, shelved, skipped)
}
func (s *catalogueUploadService) AttachSheet(batchID, sheetRows string) error {
return s.repo.AttachSheet(batchID, sheetRows)
}