Files
backend_fiesta/controllers/orderController.go
abhishek c290e1729a Route offline sales by the branch named on each spreadsheet row
The offline-sales import required one workbook per outlet and a store
picked in the UI. A merchant running several branches had to download,
fill and upload a file per branch, and the picker defaulted to the
tenant's first outlet — so an admin who never touched it silently
credited the wrong store, which no validation could catch because the
file and the selection agreed with each other.

One workbook now covers every branch. getsaletemplate takes locationid=0
(the default) to span the tenant, stamping tenantid, locationid and the
store name onto every row, and that row's locationid is what decides
which branch a sale is deducted from. The INNER JOIN on tenantlocations
confines it to outlets the tenant owns, so a template can never disclose
another merchant's catalogue.

uploadofflinesales accordingly takes locationid on each bill. The
locationid on the request itself becomes a scope constraint rather than
a destination: left at 0 the bills go where their rows say, and set to a
branch it pins the upload there and refuses anything else. That is what
holds a store user to their own store — the pin comes from their session,
so editing the locationid column in the spreadsheet changes nothing.
Every branch referenced is checked against the tenant regardless.

Branch context and catalogue are resolved once per branch and reused; a
workbook covering six outlets would otherwise re-run both queries for
every bill in it.

Duplicate detection is now per branch. Bill numbers only have to be
unique within a store, since counter books at different outlets
routinely restart numbering at 1, and treating a shared number as a
repeat would have silently dropped a real sale.

Verified against tenant 1087, whose two branches both stock product
6998 at 100 units: a single upload of two bills moved 1097 to 97 and
1135 to 95 independently; the same bill number at both branches imported
as two separate orders; an upload pinned to 1097 imported its own bill
and refused the 1135 one; a row naming another tenant's outlet was
refused; and re-uploading the file deducted nothing. All five test
orders were cancelled afterwards and both branches confirmed back at 100.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 12:17:52 +05:30

591 lines
16 KiB
Go

package controllers
import (
"fmt"
"log"
"nearle/models"
"net/http"
"strconv"
"strings"
"time"
"nearle/services"
"github.com/gofiber/fiber/v2"
)
type OrderController struct {
orderService services.OrderService
}
func NewOrderController(orderService services.OrderService) *OrderController {
return &OrderController{orderService: orderService}
}
func (ctl *OrderController) GetOrders(c *fiber.Ctx) error {
tid, _ := strconv.Atoi(c.Query("tenantid"))
pid, _ := strconv.Atoi(c.Query("partnerid"))
cid, _ := strconv.Atoi(c.Query("customerid"))
mid, _ := strconv.Atoi(c.Query("moduleid"))
aid, _ := strconv.Atoi(c.Query("applocationid"))
uid, _ := strconv.Atoi(c.Query("appuserid"))
lid, _ := strconv.Atoi(c.Query("locationid"))
configid, _ := strconv.Atoi(c.Query("configid"))
stat := c.Query("status")
fdate := c.Query("fromdate")
tdate := c.Query("todate")
keyword := c.Query("keyword")
pageno, _ := strconv.Atoi(c.Query("pageno"))
pagesize, _ := strconv.Atoi(c.Query("pagesize"))
if pageno <= 0 {
pageno = 1
}
if pagesize <= 0 {
pagesize = 10
}
// Build dynamic query struct
query := models.DeliveryQuery{
Tenantid: tid,
Partnerid: pid,
Customerid: cid,
Moduleid: mid,
Applocationid: aid,
Locationid: lid,
UserID: uid,
Appuserid: uid,
Configid: configid,
Fromdate: fdate,
ToDate: tdate,
Status: stat,
Keyword: keyword,
Pageno: pageno,
Pagesize: pagesize,
}
var (
orders []models.OrderInfo
err error
)
// --------------------------
// 🔥 DYNAMIC ROUTING LOGIC
// --------------------------
if tid != 0 && lid != 0 {
// ⭐ Both tenant & location → special handler
orders, err = ctl.orderService.GetTenantLocationOrders(query)
} else if tid != 0 {
// Tenant only
orders, err = ctl.orderService.GetTenantOrders(query)
} else if pid != 0 {
// Partner
orders, err = ctl.orderService.GetPartnerOrders(stat, fdate, tdate, pid, pageno, pagesize, keyword)
} else if cid != 0 {
// Customer
orders, err = ctl.orderService.GetCustomerOrders(stat, fdate, tdate, cid, mid, pageno, pagesize, keyword)
} else if aid != 0 {
// App-location orders
orders, err = ctl.orderService.GetAdminOrders(stat, fdate, tdate, aid, pageno, pagesize, keyword)
} else if uid != 0 {
// User orders
orders, err = ctl.orderService.GetUserOrders(stat, fdate, tdate, uid, pageno, pagesize, keyword)
} else {
// No scoping id supplied (tenantid/partnerid/customerid/applocationid/appuserid).
// Refuse instead of silently returning every order in the database.
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
"status": false,
"code": http.StatusBadRequest,
"message": "At least one of tenantid, partnerid, customerid, applocationid or appuserid is required",
})
}
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
"status": false,
"code": http.StatusInternalServerError,
"message": err.Error(),
})
}
return c.JSON(fiber.Map{
"status": true,
"code": http.StatusOK,
"message": "Success",
"details": orders,
})
}
func (ctl *OrderController) GetOrderSummary(c *fiber.Ctx) error {
tid, _ := strconv.Atoi(c.Query("tenantid"))
pid, _ := strconv.Atoi(c.Query("partnerid"))
cid, _ := strconv.Atoi(c.Query("customerid"))
lid, _ := strconv.Atoi(c.Query("locationid"))
fdate := c.Query("fromdate")
tdate := c.Query("todate")
if tid == 0 && pid == 0 && cid == 0 && lid == 0 {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
"code": http.StatusBadRequest,
"message": "At least one of tenantid, partnerid, customerid or locationid is required",
"status": false,
})
}
data, err := ctl.orderService.GetOrderSummary(tid, pid, cid, lid, fdate, tdate)
if 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,
"details": data,
})
}
func (ctl *OrderController) GetlocationOrderSummary(c *fiber.Ctx) error {
tenantIDStr := c.Query("tenantid")
tenantID, _ := strconv.Atoi(tenantIDStr)
if tenantID == 0 {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
"status": false,
"code": http.StatusBadRequest,
"message": "tenantid is required",
})
}
data, err := ctl.orderService.GetLocationOrderSummary(tenantID)
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
"status": false,
"code": http.StatusInternalServerError,
"message": err.Error(),
})
}
return c.Status(http.StatusOK).JSON(fiber.Map{
"code": http.StatusOK,
"message": "Success",
"status": true,
"details": data,
})
}
func (ctl *OrderController) GetOrderInsights(c *fiber.Ctx) error {
tenantIDStr := c.Query("tenantid")
tenantID, _ := strconv.Atoi(tenantIDStr)
insights, err := ctl.orderService.GetOrderInsights(tenantID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"status": false,
"message": "Failed to fetch order insights",
"error": err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(fiber.Map{
"status": true,
"message": "Success",
"details": insights,
})
}
func (ctl *OrderController) GetOrderDetails(c *fiber.Ctx) error {
orderHeaderIDStr := c.Query("orderheaderid")
if orderHeaderIDStr == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"code": 400,
"message": "orderheaderid is required",
"status": false,
"details": []interface{}{},
})
}
orderHeaderID, err := strconv.Atoi(orderHeaderIDStr)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"code": 400,
"message": "invalid orderheaderid",
"status": false,
"details": []interface{}{},
})
}
details, err := ctl.orderService.GetOrderDetails(orderHeaderID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"code": 500,
"message": "Failed to fetch order details",
"status": false,
"details": []interface{}{},
})
}
var orderAmount, totalTaxAmount float64
if len(details) > 0 {
orderAmount = details[0].Orderamount
totalTaxAmount = details[0].Totaltaxamount
}
return c.JSON(fiber.Map{
"code": 200,
"pricedetails": fiber.Map{
"orderamount": orderAmount,
"totaltaxamount": totalTaxAmount,
},
"details": details,
"message": "Success",
"status": true,
})
}
func (ctl *OrderController) UpdateOrder(c *fiber.Ctx) error {
var order models.Orders
if err := c.BodyParser(&order); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
"code": http.StatusBadRequest,
"message": "Invalid request body",
"status": false,
})
}
if err := ctl.orderService.UpdateOrder(&order); err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
"code": http.StatusInternalServerError,
"message": "Error updating order",
"status": false,
})
}
return c.Status(http.StatusAccepted).JSON(fiber.Map{
"code": http.StatusAccepted,
"message": "Success",
"status": true,
})
}
func (ctl *OrderController) CreateOrderv3(c *fiber.Ctx) error {
var data models.Orders
// 🛠️ Strategy 1: Try parsing direct object (for Apidog)
if err := c.BodyParser(&data); err != nil {
log.Println("BodyParser strategy 1 error:", err)
}
// 🛠️ Strategy 2: If Strategy 1 didn't find a Tenantid, try parsing wrapped object (for Mobile App)
if data.Tenantid == 0 {
type OrderWrapper struct {
Orders models.Orders `json:"orders"`
}
var wrapper OrderWrapper
if err := c.BodyParser(&wrapper); err == nil && wrapper.Orders.Tenantid != 0 {
data = wrapper.Orders
}
}
// 🛠️ Strategy 3: some clients send the header under "orders" but the line
// items as a SIBLING top-level "items" array rather than nested inside
// it. Strategy 2's OrderWrapper only has an "orders" field, so
// encoding/json silently drops that sibling key — the order header
// parses fine but data.Items ends up empty, which used to let the order
// go through with zero items and skip the stock check entirely (the
// pre-validation loop below iterates over data.Items). Pick it up here
// if strategies 1/2 left Items empty.
if len(data.Items) == 0 {
type ItemsWrapper struct {
Items []models.OrderDetail `json:"items"`
}
var itemsWrapper ItemsWrapper
if err := c.BodyParser(&itemsWrapper); err == nil && len(itemsWrapper.Items) > 0 {
data.Items = itemsWrapper.Items
}
}
// Double check we have the required ID
if data.Tenantid == 0 {
return c.Status(http.StatusConflict).JSON(fiber.Map{
"code": http.StatusConflict,
"message": "Tenant ID is required",
"status": false,
})
}
// An order with no line items has nothing to check stock against — the
// pre-validation loop in CreateOrder simply wouldn't run, silently
// creating a phantom header-only order that never deducted stock.
// Reject it outright instead.
if len(data.Items) == 0 {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
"code": http.StatusBadRequest,
"message": "Order must contain at least one item",
"status": false,
})
}
if strings.TrimSpace(data.Orderdate) == "" {
data.Orderdate = time.Now().Format("2006-01-02 15:04:05")
}
if strings.TrimSpace(data.Deliverytime) == "" {
data.Deliverytime = time.Now().Format("2006-01-02 15:04:05")
}
order, err := ctl.orderService.CreateOrder(data)
if err != nil {
log.Println("CreateOrder service error:", err)
statusCode := http.StatusInternalServerError
if strings.Contains(strings.ToLower(err.Error()), "insufficient stock") {
statusCode = http.StatusConflict
}
return c.Status(statusCode).JSON(fiber.Map{
"code": statusCode,
"message": err.Error(),
"status": false,
})
}
return c.Status(http.StatusOK).JSON(fiber.Map{
"code": http.StatusOK,
"message": "Order created successfully",
"status": true,
"details": order,
})
}
// UploadOfflineSales imports a spreadsheet of in-store counter sales.
//
// The response is 200 whenever the batch was processed, even if individual
// bills were rejected, because a partial import is a normal outcome for a
// spreadsheet and the per-bill results carry the detail. A non-200 means
// nothing at all was attempted — a malformed body, or an outlet the caller has
// no claim on.
func (ctl *OrderController) UploadOfflineSales(c *fiber.Ctx) error {
var input models.OfflineSalesUpload
if err := c.BodyParser(&input); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
"code": http.StatusBadRequest,
"message": "could not read the upload: " + err.Error(),
"status": false,
})
}
// locationid is optional: 0 means the bills carry their own branch, which
// is how one workbook covers every outlet a merchant runs. Supplying it
// pins the upload to that branch and rejects anything else in the file.
if input.Tenantid <= 0 {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
"code": http.StatusBadRequest,
"message": "tenantid is required",
"status": false,
})
}
if len(input.Bills) == 0 {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
"code": http.StatusBadRequest,
"message": "no sales rows found in the upload",
"status": false,
})
}
result, err := ctl.orderService.UploadOfflineSales(input)
if err != nil {
log.Println("UploadOfflineSales service error:", err)
// An outlet the caller doesn't own is a permission problem, not a
// server fault, and is reported as one so the UI can say so plainly.
statusCode := http.StatusInternalServerError
if strings.Contains(strings.ToLower(err.Error()), "does not belong to tenant") {
statusCode = http.StatusForbidden
}
return c.Status(statusCode).JSON(fiber.Map{
"code": statusCode,
"message": err.Error(),
"status": false,
})
}
message := fmt.Sprintf("%d bill(s) imported", result.Imported)
if result.Duplicate > 0 {
message += fmt.Sprintf(", %d already imported", result.Duplicate)
}
if result.Failed > 0 {
message += fmt.Sprintf(", %d failed", result.Failed)
}
return c.Status(http.StatusOK).JSON(fiber.Map{
"code": http.StatusOK,
"message": message,
"status": true,
"details": result,
})
}
func (ctl *OrderController) GetCustomerOrders(c *fiber.Ctx) error {
customerID := c.Query("customerid")
tenantID := c.Query("tenantid")
moduleID := c.Query("moduleid")
fromDate := c.Query("fromdate")
toDate := c.Query("todate")
orderStatus := c.Query("orderstatus")
keyword := c.Query("keyword")
pageNo, _ := strconv.Atoi(c.Query("pageno", "1"))
pageSize, _ := strconv.Atoi(c.Query("pagesize", "10"))
if pageNo < 1 {
pageNo = 1
}
if pageSize < 1 {
pageSize = 10
}
offset := (pageNo - 1) * pageSize
if customerID == "" {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
"code": http.StatusBadRequest,
"status": false,
"message": "customerid is required",
})
}
orders, err := ctl.orderService.GetCustomerOrdersv3(customerID, tenantID, moduleID, fromDate, toDate, orderStatus, keyword, pageSize, offset)
if err != nil {
log.Println("GetCustomerOrders error:", err)
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
"code": http.StatusInternalServerError,
"status": false,
"message": "Failed to fetch customer orders",
"error": err.Error(),
})
}
return c.JSON(fiber.Map{
"code": http.StatusOK,
"status": true,
"message": "Customer orders fetched successfully",
"data": orders,
})
}
func (ctl *OrderController) GetRevenueSummary(c *fiber.Ctx) error {
tid, _ := strconv.Atoi(c.Query("tenantid"))
lid, _ := strconv.Atoi(c.Query("locationid"))
fdate := c.Query("fromdate")
tdate := c.Query("todate")
if tid == 0 && lid == 0 {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
"code": http.StatusBadRequest,
"message": "Either tenantid or locationid query parameter is required",
"status": false,
})
}
data, err := ctl.orderService.GetRevenueSummary(tid, lid, fdate, tdate)
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
"code": http.StatusInternalServerError,
"message": err.Error(),
"status": false,
})
}
return c.Status(http.StatusOK).JSON(fiber.Map{
"code": http.StatusOK,
"message": "Success",
"status": true,
"details": data,
})
}
func (ctl *OrderController) GetSalesSummary(c *fiber.Ctx) error {
tid, _ := strconv.Atoi(c.Query("tenantid"))
lid, _ := strconv.Atoi(c.Query("locationid"))
fdate := c.Query("fromdate")
tdate := c.Query("todate")
if tid == 0 {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
"code": http.StatusBadRequest,
"message": "tenantid query parameter is required",
"status": false,
})
}
data, err := ctl.orderService.GetSalesSummary(tid, lid, fdate, tdate)
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
"code": http.StatusInternalServerError,
"message": err.Error(),
"status": false,
})
}
return c.Status(http.StatusOK).JSON(fiber.Map{
"code": http.StatusOK,
"message": "Success",
"status": true,
"details": data,
})
}
func (ctl *OrderController) GetTimeSeries(c *fiber.Ctx) error {
tid, _ := strconv.Atoi(c.Query("tenantid"))
lid, _ := strconv.Atoi(c.Query("locationid"))
granularity := c.Query("granularity")
fdate := c.Query("fromdate")
tdate := c.Query("todate")
if tid == 0 {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
"code": http.StatusBadRequest,
"message": "tenantid query parameter is required",
"status": false,
})
}
if granularity == "" {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
"code": http.StatusBadRequest,
"message": "granularity query parameter is required (day, month, year)",
"status": false,
})
}
data, err := ctl.orderService.GetTimeSeries(tid, lid, granularity, fdate, tdate)
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
"code": http.StatusInternalServerError,
"message": err.Error(),
"status": false,
})
}
return c.Status(http.StatusOK).JSON(fiber.Map{
"code": http.StatusOK,
"message": "Success",
"status": true,
"details": data,
})
}