package controllers import ( "fmt" "net/http" "strconv" "strings" "nearle/models" "nearle/services" "github.com/gofiber/fiber/v2" ) type ProductController struct { productService services.ProductService } func NewProductController(productService services.ProductService) *ProductController { return &ProductController{productService: productService} } func (ctl *ProductController) GetProductSubCategory(c *fiber.Ctx) error { categoryID, _ := strconv.Atoi(c.Query("categoryid", "0")) tenantID, _ := strconv.Atoi(c.Query("tenantid", "0")) data, err := ctl.productService.GetProductSubCategory(categoryID, tenantID) if err != nil { return c.JSON(fiber.Map{ "code": http.StatusInternalServerError, "message": err.Error(), "status": false, }) } return c.JSON(fiber.Map{ "code": 200, "message": "Success", "status": true, "details": data, }) } func (ctl *ProductController) GetProductCount(c *fiber.Ctx) error { tenantID, _ := strconv.Atoi(c.Query("tenantid")) categoryID, _ := strconv.Atoi(c.Query("categoryid")) subcategoryID, _ := strconv.Atoi(c.Query("subcategoryid")) // locationID, _ := strconv.Atoi(c.Query("locationid")) approve := c.Query("approve", "") data, err := ctl.productService.GetProductCount(tenantID, categoryID, subcategoryID, approve) if err != nil { return c.JSON(fiber.Map{ "code": http.StatusConflict, "message": "failed", "status": false, }) } return c.JSON(fiber.Map{ "code": http.StatusOK, "message": "Success", "status": true, "details": data, }) } func (ctl *ProductController) GetProductCategory(c *fiber.Ctx) error { data, err := ctl.productService.GetProductCategory() if err != nil { return c.JSON(fiber.Map{ "code": 500, "message": "Failed to fetch categories", "status": false, }) } return c.JSON(fiber.Map{ "code": http.StatusOK, "message": "Success", "status": true, "details": data, }) } func (ctl *ProductController) GetProductVariants(c *fiber.Ctx) error { tenantID, _ := strconv.Atoi(c.Query("tenantid")) subcategoryID, _ := strconv.Atoi(c.Query("subcategoryid")) data, err := ctl.productService.GetProductVariants(tenantID, subcategoryID) if err != nil { return c.JSON(fiber.Map{ "code": 500, "message": "Failed to fetch Product varients", "status": false, }) } return c.JSON(fiber.Map{ "code": http.StatusOK, "message": "Success", "status": true, "details": data, }) } func (ctl *ProductController) GetCatalougeProducts(c *fiber.Ctx) error { tenantID, _ := strconv.Atoi(c.Query("tenantid")) locationID, _ := strconv.Atoi(c.Query("locationid")) subcategoryID, _ := strconv.Atoi(c.Query("subcategoryid")) keyword := c.Query("keyword") pageno, _ := strconv.Atoi(c.Query("pageno")) pagesize, _ := strconv.Atoi(c.Query("pagesize")) data, err := ctl.productService.GetCatalougeProducts(tenantID, locationID, subcategoryID, pageno, pagesize, keyword) if err != nil { return c.JSON(fiber.Map{ "code": 500, "message": "Failed to fetch Catalouge Product", "status": false, }) } return c.JSON(fiber.Map{ "code": http.StatusOK, "message": "Success", "status": true, "details": data, }) } func (ctl *ProductController) GetProductStocks(c *fiber.Ctx) error { tenantID := c.Query("tenantid") locationID := c.Query("locationid") stocks, err := ctl.productService.GetProductStocks(tenantID, locationID) if err != nil { return c.JSON(fiber.Map{ "code": 500, "message": "Failed to fetch product stocks", "status": false, }) } return c.JSON(fiber.Map{ "code": 200, "message": "Product stocks fetched successfully", "status": true, "data": stocks, }) } func (ctl *ProductController) CreateProductStock(c *fiber.Ctx) error { var stocks []models.Productstock if err := c.BodyParser(&stocks); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "code": 400, "message": "Invalid request body", "status": false, }) } if err := ctl.productService.CreateProductStock(stocks); err != nil { return c.JSON(fiber.Map{ "code": 500, "message": "Failed to create product stocks", "status": false, }) } return c.JSON(fiber.Map{ "code": 201, "message": "Product stocks created successfully", "status": true, "details": stocks, }) } func (ctl *ProductController) CreateProduct(c *fiber.Ctx) error { var product models.Products if err := c.BodyParser(&product); err != nil { return c.JSON(fiber.Map{ "code": http.StatusBadRequest, "message": "Invalid request body", "status": false, }) } // The created row, not the parsed body. // // This returned the struct it had just parsed off the request, which by // definition carried `productid: 0` — the id is assigned by the database a // moment later and was never read back. Every caller that needed the id // went and looked the product up again by SKU. created, err := ctl.productService.CreateProduct(product) if err != nil { return c.JSON(fiber.Map{ "code": http.StatusInternalServerError, "message": "Failed to create product", "status": false, }) } return c.JSON(fiber.Map{ "code": http.StatusCreated, "message": "Product created successfully", "status": true, "data": created, }) } func (ctl *ProductController) UpdateProduct(c *fiber.Ctx) error { var product models.Products if err := c.BodyParser(&product); err != nil { return c.JSON(fiber.Map{ "code": http.StatusBadRequest, "message": "Invalid request body", "status": false, }) } if err := ctl.productService.UpdateProduct(product); err != nil { return c.JSON(fiber.Map{ "code": http.StatusConflict, "message": err.Error(), "status": false, }) } return c.JSON(fiber.Map{ "code": http.StatusAccepted, "message": "Product updated successfully", "status": true, "data": product, }) } func (ctl *ProductController) DeleteProduct(c *fiber.Ctx) error { pidStr := c.Query("productid") pid, err := strconv.Atoi(pidStr) if err != nil || pid <= 0 { return c.JSON(fiber.Map{ "code": http.StatusBadRequest, "message": "Invalid product ID", "status": false, }) } if err := ctl.productService.DeleteProduct(pid); err != nil { return c.JSON(fiber.Map{ "code": http.StatusInternalServerError, "message": err.Error(), "status": false, }) } return c.JSON(fiber.Map{ "code": http.StatusOK, "message": "Product deleted successfully", "status": true, }) } func (ctl *ProductController) GetStockStatement(c *fiber.Ctx) error { tenantID, _ := strconv.Atoi(c.Query("tenantid")) locationID, _ := strconv.Atoi(c.Query("locationid")) subcategoryID, _ := strconv.Atoi(c.Query("subcategoryid")) pageno, _ := strconv.Atoi(c.Query("pageno")) pagesize, _ := strconv.Atoi(c.Query("pagesize")) keyword := c.Query("keyword") data, err := ctl.productService.GetStockStatement(tenantID, locationID, subcategoryID, pageno, pagesize, keyword) if err != nil { return c.JSON(fiber.Map{ "code": http.StatusInternalServerError, "message": err.Error(), "status": false, }) } return c.JSON(fiber.Map{ "code": http.StatusOK, "message": "Success", "status": true, "details": data, }) } func (ctl *ProductController) GetLocationProducts(c *fiber.Ctx) error { tenantID, _ := strconv.Atoi(c.Query("tenantid")) locationID, _ := strconv.Atoi(c.Query("locationid")) subcategoryID, _ := strconv.Atoi(c.Query("subcategoryid")) keyword := c.Query("keyword") pageno, _ := strconv.Atoi(c.Query("pageno")) pagesize, _ := strconv.Atoi(c.Query("pagesize")) result, err := ctl.productService.GetLocationProducts(tenantID, locationID, subcategoryID, pageno, pagesize, keyword) if err != nil { return c.JSON(fiber.Map{ "status": false, "code": http.StatusInternalServerError, "message": err.Error(), }) } return c.JSON(fiber.Map{ "status": true, "code": http.StatusOK, "message": "Success", "details": result, }) } // GetSaleTemplate serves the data the web app turns into the offline-sales // spreadsheet. // // locationid is optional and defaults to 0, meaning every branch the tenant // runs — one workbook for the whole business, with each row carrying the branch // its stock belongs to. A store user passes their own locationid to get just // theirs. tenantid is required: without it there is no scope at all. func (ctl *ProductController) GetSaleTemplate(c *fiber.Ctx) error { tenantID, _ := strconv.Atoi(c.Query("tenantid")) locationID, _ := strconv.Atoi(c.Query("locationid", "0")) if tenantID <= 0 { return c.JSON(fiber.Map{ "status": false, "code": http.StatusBadRequest, "message": "tenantid is required", }) } result, err := ctl.productService.GetSaleTemplate(tenantID, locationID) if err != nil { return c.JSON(fiber.Map{ "status": false, "code": http.StatusInternalServerError, "message": err.Error(), }) } return c.JSON(fiber.Map{ "status": true, "code": http.StatusOK, "message": "Success", "details": result, }) } func (ctl *ProductController) GetLocationProductSummary(c *fiber.Ctx) error { tenantID, _ := strconv.Atoi(c.Query("tenantid")) locationID, _ := strconv.Atoi(c.Query("locationid")) result, err := ctl.productService.GetLocationProductSummary(tenantID, locationID) if err != nil { return c.JSON(fiber.Map{ "status": false, "code": http.StatusInternalServerError, "message": err.Error(), }) } return c.JSON(fiber.Map{ "status": true, "code": http.StatusOK, "message": "Success", "details": result, }) } func (ctl *ProductController) GetAllProducts(c *fiber.Ctx) error { categoryID, _ := strconv.Atoi(c.Query("categoryid")) subcategoryID, _ := strconv.Atoi(c.Query("subcategoryid")) productID, _ := strconv.Atoi(c.Query("productid")) applocationID, _ := strconv.Atoi(c.Query("applocationid")) tenantID, _ := strconv.Atoi(c.Query("tenantid")) locationID, _ := strconv.Atoi(c.Query("locationid")) keyword := c.Query("keyword", "") productStatus := c.Query("productstatus", "") approve := c.Query("approve", "") pageno, _ := strconv.Atoi(c.Query("pageno")) pagesize, _ := strconv.Atoi(c.Query("pagesize")) details, err := ctl.productService.FetchFilteredProducts( categoryID, subcategoryID, productID, applocationID, tenantID, locationID, keyword, productStatus, approve, pageno, pagesize, ) // The customer app is not shown what the shop cannot sell. // // Scoped to the /v1/mob base rather than applied in the service, because // this one handler answers on BOTH bases and the two callers want opposite // things. The console reads it to restock — an empty line is exactly what a // merchant needs to see and act on, so filtering there would hide the work. // A shopper reading the same list can only be misled by it: measured // 2026-09-02 on R mart, four products were on offer in the app with a zero // balance, including Cadbury Bournvita 500g and two Amul packs. Ordering one // gets a 409 at checkout, after the shopper has chosen it. // // It filters on Productstock, the same number the response carries, so the // list and the figure beside it cannot disagree. if err == nil && isAppRequest(c) { details = services.InStockOnlyGrouped(details) // And nothing a shopper cannot be charged for. An unpriced product is // unfinished, not free, and ₹0 on a phone reads as the latter. details = services.PricedOnly(details) // One product, three sizes — not three products. // // A shop stocking Aachi Baby Fryums in 100g, 500g and 1kg has three // product rows, each with its own price and its own shelf. The shopper // should meet ONE listing and pick the size; the sizes travel with the // parent in `variantoptions`, so nothing becomes unreachable. // // Best-effort, like the stock filter above: if the lookup fails the // shopper sees the three rows they see today, which is worse than the // grouping and much better than an error. if isChild, cErr := ctl.productService.VariantChildIDs(tenantID); cErr == nil { details = services.WithoutVariantChildren(details, isChild) } } if err != nil { return c.JSON(fiber.Map{ "status": false, "code": http.StatusConflict, "message": err.Error(), }) } return c.JSON(fiber.Map{ "status": true, "code": http.StatusOK, "message": "Success", "data": details, }) } func (ctl *ProductController) GetProductByVariant(c *fiber.Ctx) error { tenantID, _ := strconv.Atoi(c.Query("tenantid")) variantid, _ := strconv.Atoi(c.Query("variantid")) locationID, _ := strconv.Atoi(c.Query("locationid")) // `productid` is the parameter the ordering screen should send. // // With it the caller no longer has to know whether the thing it tapped has // sizes: a product in a variant group comes back with the whole group to // choose from, a product in none comes back on its own. Previously only // `variantid` was accepted, so an ungrouped product — `variants = 0`, which // is most of them — could not be requested at all and the order screen had // nothing to work with. productID, _ := strconv.Atoi(c.Query("productid")) // One of the two has to name something, and `variantid=0` names nothing. // // 0 is not a variant group — it is the value every UNGROUPED product // carries, so a query for it used to match the tenant's whole ungrouped // catalogue and return six unrelated products as each other's variants. The // ordering screen then had a list it could not choose from, and no order // could be placed. Refusing here turns that into one sentence naming the // parameter to send instead. if variantid <= 0 && productID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "status": false, "message": "send productid (preferred) or a variantid above 0 — variantid 0 is not a variant group, it is what every ungrouped product carries", }) } result, err := ctl.productService.GetProductByVariant(tenantID, variantid, locationID, productID) if err != nil { return c.JSON(fiber.Map{ "code": http.StatusConflict, "message": "failed", "status": false, }) } return c.JSON(fiber.Map{ "code": http.StatusOK, "message": "Success", "status": true, "details": result, }) } func (ctl *ProductController) GetProductsBySubcategory(c *fiber.Ctx) error { categoryID, err := strconv.Atoi(c.Query("categoryid")) if err != nil || categoryID == 0 { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "code": 400, "status": false, "message": "Valid categoryid is required", "data": fiber.Map{}, }) } params := models.ProductFilter{ CategoryID: categoryID, TenantID: parseInt(c.Query("tenantid")), AppLocationID: parseInt(c.Query("applocationid")), ProductID: parseInt(c.Query("productid")), Keyword: c.Query("keyword"), LocationID: parseInt(c.Query("locationid")), } result, err := ctl.productService.GetProductsBySubcategory(params) if err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{ "code": 500, "status": false, "message": err.Error(), "data": fiber.Map{}, }) } return c.JSON(fiber.Map{ "code": 200, "status": true, "message": "Success", "data": result, }) } func parseInt(s string) int { if s == "" { return 0 } val, _ := strconv.Atoi(s) return val } func (ctl *ProductController) UpdateProductLocation(c *fiber.Ctx) error { var data models.Productlocations if err := c.BodyParser(&data); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "status": false, "code": http.StatusBadRequest, "message": "Invalid request body", }) } if err := ctl.productService.UpdateProductLocation(data); err != nil { return c.Status(http.StatusConflict).JSON(fiber.Map{ "status": false, "code": http.StatusConflict, "message": err.Error(), }) } return c.Status(http.StatusAccepted).JSON(fiber.Map{ "status": true, "code": http.StatusAccepted, "message": "Product update successful", }) } func (ctl *ProductController) CreateProductLocation(c *fiber.Ctx) error { var data []models.Productlocations if err := c.BodyParser(&data); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "message": "Invalid request body", "status": false, }) } if err := ctl.productService.CreateProductLocation(data); err != nil { return c.Status(http.StatusConflict).JSON(fiber.Map{ "status": false, "code": http.StatusConflict, "message": err.Error(), }) } return c.Status(http.StatusCreated).JSON(fiber.Map{ "status": true, "code": http.StatusCreated, "message": "Success", }) } // UpdateProductVariant puts a product into a variant group, or takes it out. // // variantid 0 means ungrouped and is allowed — it is a real thing to want. The // read path is what refuses to treat 0 as a group to search for. func (ctl *ProductController) UpdateProductVariant(c *fiber.Ctx) error { var body struct { Productid int `json:"productid"` Variantid int `json:"variantid"` } if err := c.BodyParser(&body); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "status": false, "message": "Invalid request body", }) } if body.Productid <= 0 { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "status": false, "message": "productid is required", }) } if err := ctl.productService.UpdateProductVariant(body.Productid, body.Variantid); err != nil { return c.Status(fiber.StatusConflict).JSON(fiber.Map{ "code": http.StatusConflict, "status": false, "message": err.Error(), }) } return c.JSON(fiber.Map{"code": http.StatusOK, "status": true, "message": "Success"}) } func (ctl *ProductController) CreateProductVariant(c *fiber.Ctx) error { var input models.Productvariant // Parse request body if err := c.BodyParser(&input); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "message": "Invalid request body", "status": false, }) } // Call service layer if err := ctl.productService.CreateProductVariant(input); err != nil { return c.Status(http.StatusConflict).JSON(fiber.Map{ "code": http.StatusConflict, "message": err.Error(), "status": false, }) } return c.Status(http.StatusOK).JSON(fiber.Map{ "code": http.StatusOK, "message": "Success", "status": true, }) } func (ctl *ProductController) ImportCatalogueProduct(c *fiber.Ctx) error { var data []models.ImportCatalogueProductRequest if err := c.BodyParser(&data); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "message": "Invalid request body", "status": false, }) } if len(data) == 0 { return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "message": "Request body must contain at least one product", "status": false, }) } for _, req := range data { // `categoryid` IS required, and the comment that used to sit here // argued the opposite. It said an unclassified product is "visibly // unfinished" and therefore harmless, and that importing "reaches no // shop and cannot be sold". // // Both halves were wrong, and a sweep of all 262 tenants measured it: // 7 products across 3 tenants sat with categoryid 0, and 6 outlets were // serving shoppers an EMPTY shop while their consoles listed stock. // Nothing was visibly unfinished — `getlocationproducts` does not filter // on category, so the product looked entirely normal to the merchant. // And import DOES reach a shop: it writes productlocations. // // The state was also unrecoverable. GetProductsBySubcategory rejects // categoryid 0 outright, `UpdateProduct` writes only // productlocations.status, and re-import corrected pricing alone — so // no request could put it right. Re-import now repairs the category // (productService.ImportCatalogueProduct), and this refuses to create // the state in the first place. // // A wrongly filed product remains the better failure: it is findable, // and it is fixable by re-importing. An unfiled one was neither. // Named individually rather than as one list of five. // // The old message recited every required field whichever one was // actually missing, so a request rejected for `categoryid: 0` read as // "catalogueid is required" and sent somebody looking at the wrong // field entirely. An error that does not say what is wrong costs more // than the branch it saves. var missing []string if req.Tenantid == 0 { missing = append(missing, "tenantid") } if req.Locationid == 0 { missing = append(missing, "locationid") } if req.Brand == "" { missing = append(missing, "brand") } if req.Catalogueid == 0 { missing = append(missing, "catalogueid") } if req.Categoryid == 0 { missing = append(missing, "categoryid") } if len(missing) > 0 { return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "message": fmt.Sprintf("missing required field(s): %s", strings.Join(missing, ", ")), "status": false, }) } } if err := ctl.productService.ImportCatalogueProduct(data); err != nil { return c.Status(http.StatusConflict).JSON(fiber.Map{ "status": false, "code": http.StatusConflict, "message": err.Error(), }) } return c.Status(http.StatusCreated).JSON(fiber.Map{ "status": true, "code": http.StatusCreated, "message": "Success", }) } func (ctl *ProductController) GetImportedCatalogueProducts(c *fiber.Ctx) error { tenantID, _ := strconv.Atoi(c.Query("tenantid")) // brand is optional: omit it to check imported status across every // brand at once, matching the all-brands catalogue browse view. brand := c.Query("brand") if tenantID == 0 { return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "message": "tenantid is required", "status": false, }) } refs, err := ctl.productService.GetImportedCatalogueRefs(tenantID, brand) if err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{ "code": http.StatusInternalServerError, "message": err.Error(), "status": false, }) } return c.JSON(fiber.Map{ "code": http.StatusOK, "message": "Success", "status": true, "details": refs, }) } func (ctl *ProductController) GetTenantCategories(c *fiber.Ctx) error { tenantID, _ := strconv.Atoi(c.Query("tenantid")) if tenantID == 0 { return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "message": "tenantid is required", "status": false, }) } categories, err := ctl.productService.GetTenantCategories(tenantID) if err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{ "code": http.StatusInternalServerError, "message": err.Error(), "status": false, }) } return c.JSON(fiber.Map{ "code": http.StatusOK, "message": "Success", "status": true, "details": categories, }) } // Recategorise re-files a tenant's products in bulk. // // PUT and not POST: it changes rows that already exist and creates none. The // response carries how many rows actually moved, which is the only way a caller // can tell "all done" from "every id I sent was wrong". func (ctl *ProductController) Recategorise(c *fiber.Ctx) error { var body struct { Tenantid int `json:"tenantid"` Updates []models.ProductCategoryUpdate `json:"updates"` } if err := c.BodyParser(&body); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "message": "invalid body", "status": false, }) } if body.Tenantid == 0 { return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "message": "tenantid is required", "status": false, }) } moved, err := ctl.productService.RecategoriseProducts(body.Tenantid, body.Updates) if err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{ "code": http.StatusInternalServerError, "message": err.Error(), "status": false, }) } return c.JSON(fiber.Map{ "code": http.StatusOK, "message": "Success", "status": true, "details": fiber.Map{"moved": moved}, }) } // ResolveCategories exchanges category names for this tenant's category ids. // // POST, not GET, because it CREATES the categories it cannot find — a sheet // naming an aisle this shop has never stocked should not fail, it should open // the aisle. The response maps the name a caller sent to the id it must store. // // Keyed on the lowercased, trimmed name, so a caller can look up whatever // casing its own sheet used without having to guess how it was filed. func (ctl *ProductController) ResolveCategories(c *fiber.Ctx) error { var body struct { Tenantid int `json:"tenantid"` Names []string `json:"names"` } if err := c.BodyParser(&body); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "message": "invalid body", "status": false, }) } if body.Tenantid == 0 { return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "message": "tenantid is required", "status": false, }) } resolved, err := ctl.productService.EnsureTenantCategories(body.Tenantid, body.Names) if err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{ "code": http.StatusInternalServerError, "message": err.Error(), "status": false, }) } return c.JSON(fiber.Map{ "code": http.StatusOK, "message": "Success", "status": true, "details": resolved, }) } func (ctl *ProductController) DeleteProductLocation(c *fiber.Ctx) error { var input struct { Tenantid int `json:"tenantid"` Locationid int `json:"locationid"` Productid int `json:"productid"` } if err := c.BodyParser(&input); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "message": "Invalid request body", "status": false, }) } if input.Tenantid == 0 || input.Locationid == 0 || input.Productid == 0 { return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "message": "tenantid, locationid, and productid are required", "status": false, }) } if err := ctl.productService.DeleteProductLocation(input.Tenantid, input.Locationid, input.Productid); err != nil { return c.Status(http.StatusConflict).JSON(fiber.Map{ "code": http.StatusConflict, "message": err.Error(), "status": false, }) } return c.Status(http.StatusOK).JSON(fiber.Map{ "code": http.StatusOK, "message": "Product location deleted successfully", "status": true, }) } // PublishProduct releases a product from the admin catalogue to every outlet. // // The two rules this enforces — a price is required, and publishing covers the // whole tenant — live in the repository, so they hold for any caller rather // than only for the console form that happens to ask nicely. func (ctl *ProductController) PublishProduct(c *fiber.Ctx) error { var input struct { Tenantid int `json:"tenantid"` Productid int `json:"productid"` Price float64 `json:"price"` // Optional. Omitted leaves the product at 0%, which is a real rate for // staples rather than a missing value — so it is written as given // rather than skipped when zero. Taxpercent float64 `json:"taxpercent"` } if err := c.BodyParser(&input); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "message": "Invalid request body", "status": false, }) } outlets, err := ctl.productService.PublishProduct(input.Tenantid, input.Productid, input.Price, input.Taxpercent) if err != nil { // 400 rather than 500: every failure here is something the caller can // act on — no price, no outlets, or a product that is not theirs. return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "message": err.Error(), "status": false, }) } return c.JSON(fiber.Map{ "code": http.StatusOK, "status": true, "message": fmt.Sprintf("Published to %d outlet(s)", outlets), "details": fiber.Map{"productid": input.Productid, "outlets": outlets}, }) } // UnpublishProduct withdraws a product from every shop, keeping its rows. func (ctl *ProductController) UnpublishProduct(c *fiber.Ctx) error { var input struct { Tenantid int `json:"tenantid"` Productid int `json:"productid"` } if err := c.BodyParser(&input); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "message": "Invalid request body", "status": false, }) } outlets, err := ctl.productService.UnpublishProduct(input.Tenantid, input.Productid) if err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "message": err.Error(), "status": false, }) } return c.JSON(fiber.Map{ "code": http.StatusOK, "status": true, "message": fmt.Sprintf("Withdrawn from %d outlet(s)", outlets), "details": fiber.Map{"productid": input.Productid, "outlets": outlets}, }) } // RelinkCatalogue repairs a tenant's pointers back into the global catalogue. // // Defaults to a DRY RUN. The pass rewrites the column that decides what a // browse screen claims a shop already holds, and clearing a link is not // reversible from the outside — so the whole plan should be readable before any // of it happens. Pass `apply=true` to write. func (ctl *ProductController) RelinkCatalogue(c *fiber.Ctx) error { tenantID, _ := strconv.Atoi(c.Query("tenantid", "0")) if tenantID <= 0 { return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "message": "tenantid is required — this repairs one merchant's links, not the platform's", "status": false, }) } // Anything but an explicit `apply=true` is a dry run, including a typo. apply := c.Query("apply", "") == "true" report, err := ctl.productService.RelinkCatalogue(tenantID, !apply) if err != nil { return c.JSON(fiber.Map{"code": http.StatusInternalServerError, "message": err.Error(), "status": false}) } return c.JSON(fiber.Map{"code": 200, "message": "Success", "status": true, "details": report}) }