package controllers import ( "net/http" "strconv" "strings" "nearle/models" "nearle/services" "github.com/gofiber/fiber/v2" ) type CatalogueUploadController struct { service services.CatalogueUploadService } func NewCatalogueUploadController(service services.CatalogueUploadService) *CatalogueUploadController { return &CatalogueUploadController{service: service} } // RecordUpload stores the receipt for a spreadsheet just accepted by the ingest // service. // // Called the moment the drop is accepted, before any polling — that is the one // instant the batch id is guaranteed to exist and guaranteed not to have been // lost. Everything after it (the review wait, the run, the shelving) can be // recovered from the id; the id itself cannot be recovered from anything. func (ctl *CatalogueUploadController) RecordUpload(c *fiber.Ctx) error { var input models.CatalogueUpload 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) == "" { return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "message": "batchid is required — it is the only credential for reading the result back", "status": false, }) } if input.Tenantid <= 0 { // A receipt with no tenant belongs to nobody and would be visible to // every store login that asks for "all". Refused rather than stored // unscoped. return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "message": "tenantid is required — a receipt has to belong to a merchant", "status": false, }) } if err := ctl.service.Record(&input); err != nil { return c.JSON(fiber.Map{"code": http.StatusInternalServerError, "message": err.Error(), "status": false}) } return c.JSON(fiber.Map{"code": 200, "message": "Upload recorded", "status": true, "details": input}) } // GetUploads lists receipts for whoever is asking. // // `tenantid=0` means every tenant and is how a Nearle Admin sees the platform. // The console decides which to send from the session — the same rule the import // itself follows — because a store login reading another merchant's receipts // would see what that merchant stocks. func (ctl *CatalogueUploadController) GetUploads(c *fiber.Ctx) error { tenantID, _ := strconv.Atoi(c.Query("tenantid", "0")) locationID, _ := strconv.Atoi(c.Query("locationid", "0")) pageNo, _ := strconv.Atoi(c.Query("pageno", "1")) pageSize, _ := strconv.Atoi(c.Query("pagesize", "50")) data, err := ctl.service.List(tenantID, locationID, pageNo, pageSize) if err != nil { return c.JSON(fiber.Map{"code": http.StatusInternalServerError, "message": err.Error(), "status": false}) } // Never null. An empty list is a legitimate answer — a shop that has never // uploaded — and a JSON null here makes every caller guard a case that is // really just "none yet". if data == nil { data = []models.CatalogueUpload{} } return c.JSON(fiber.Map{"code": 200, "message": "Success", "status": true, "details": data}) } // UpdateUpload caches what the ingest service last reported. // // Written by the browser that is polling, because that is where the reading // happens: the batch id is the credential and the read is anonymous, so the // person waiting is already holding the answer. This endpoint only writes it // down so the next person does not have to wait for it again. func (ctl *CatalogueUploadController) UpdateUpload(c *fiber.Ctx) error { var input models.CatalogueUpload 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) == "" { return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "message": "batchid is required", "status": false, }) } if err := ctl.service.UpdateStatus(&input); err != nil { return c.JSON(fiber.Map{"code": http.StatusInternalServerError, "message": err.Error(), "status": false}) } return c.JSON(fiber.Map{"code": 200, "message": "Upload updated", "status": true}) } // MarkShelved records that the products reached a branch's shelf with a price // and their opening stock. // // Separate from UpdateUpload on purpose. The ingest service confirms the global // catalogue, which every merchant shares and which therefore carries no price // and no stock — so "added" and "on sale here" are two different claims, made by // two different systems, and a receipt that ran them together would report // products as sellable that no customer can buy. func (ctl *CatalogueUploadController) MarkShelved(c *fiber.Ctx) error { var input struct { Batchid string `json:"batchid"` Shelved int `json:"shelved"` Skipped int `json:"skipped"` } 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) == "" { return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "message": "batchid is required", "status": false, }) } if err := ctl.service.MarkShelved(input.Batchid, input.Shelved, input.Skipped); err != nil { return c.JSON(fiber.Map{"code": http.StatusInternalServerError, "message": err.Error(), "status": false}) } 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}) }