Files
backend_fiesta/controllers/productController.go
Suriya fab06bb33e Bridge global catalogue DB to per-store product catalogue
Adds a separate CatalogueDB (pgvector) connection alongside the main
nearledb, plus a new catalogue module (repository/service/controller/
routes) to browse it by brand, category, and keyword, with brand
optional so the whole ~237-product catalogue can be browsed unfiltered.

Adds the actual bridge: importing a catalogue product snapshots it into
the tenant's own products table (keyed on brand+catalogueid, since a
catalogue row's bare id is only unique within its own brand table),
then links it via the existing productlocations upsert. Re-importing
tops up stock and refreshes price instead of duplicating. Also adds an
imported-refs endpoint so the frontend can badge already-imported items
without diffing full product lists, and wires the new AWS S3 image
store used to resolve catalogue product photos.

Bumps Go/Docker to 1.24 for the AWS SDK dependency this needs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 17:34:20 +05:30

630 lines
16 KiB
Go

package controllers
import (
"net/http"
"strconv"
"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,
})
}
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,
)
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"))
result, err := ctl.productService.GetProductByVariant(tenantID, variantid)
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",
})
}
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 {
if req.Tenantid == 0 || req.Locationid == 0 || req.Brand == "" || req.Catalogueid == 0 || req.Categoryid == 0 || req.Subcategoryid == 0 {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
"code": http.StatusBadRequest,
"message": "tenantid, locationid, brand, catalogueid, categoryid, and subcategoryid are required",
"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) 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,
})
}