diff --git a/controllers/catalogueUploadController.go b/controllers/catalogueUploadController.go index 7b0e522..fb85323 100644 --- a/controllers/catalogueUploadController.go +++ b/controllers/catalogueUploadController.go @@ -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}) +} diff --git a/main.go b/main.go index 207440c..4ba51c7 100644 --- a/main.go +++ b/main.go @@ -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) diff --git a/models/catalogueupload.go b/models/catalogueupload.go index b28349d..8ba6c54 100644 --- a/models/catalogueupload.go +++ b/models/catalogueupload.go @@ -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"` diff --git a/repositories/catalogueUploadRepository.go b/repositories/catalogueUploadRepository.go index da8b569..65581d7 100644 --- a/repositories/catalogueUploadRepository.go +++ b/repositories/catalogueUploadRepository.go @@ -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 +} diff --git a/routes/uploadroutes.go b/routes/uploadroutes.go index c634942..aac1702 100644 --- a/routes/uploadroutes.go +++ b/routes/uploadroutes.go @@ -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) } diff --git a/services/catalogueUploadService.go b/services/catalogueUploadService.go index a90785f..99a8845 100644 --- a/services/catalogueUploadService.go +++ b/services/catalogueUploadService.go @@ -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) +}