962 lines
28 KiB
Go
962 lines
28 KiB
Go
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,
|
|
})
|
|
}
|
|
|
|
if err := ctl.productService.CreateProduct(product); 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": product,
|
|
})
|
|
}
|
|
|
|
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,
|
|
})
|
|
}
|
|
|
|
// 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})
|
|
}
|