588 lines
16 KiB
Go
588 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,
|
|
})
|
|
}
|
|
|
|
if input.Tenantid <= 0 || input.Locationid <= 0 {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"code": http.StatusBadRequest,
|
|
"message": "tenantid and locationid are 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,
|
|
})
|
|
}
|