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, }) }