Add read endpoints for counter sales
The ingest only ever wrote. A bill that reached pos_orders was safe and completely unreachable — no screen in the product could show it, and the only way to see a day's counter takings was to query the database by hand. Three endpoints: a paged bill list, one bill with its lines, and a summary split the three ways somebody actually asks for — by tender for reconciling a drawer, by day for a chart, by till for an outlet running several counters. locationid is required on all of them and is the authorisation boundary, so a caller cannot page through another shop's takings by omitting a parameter. Fetching a bill under the wrong outlet returns 404 even when the reference is a real one. Dates match businessdate rather than arrival, because a till that was offline overnight uploads yesterday's bills this morning and they belong to yesterday. The list is ordered by billedat for the same reason — sorting by arrival would interleave a recovered backlog through today. Unlike the ingest handlers these answer in the usual envelope: they are read by the web app, not by a terminal, and nothing about them is bound to the till's contract. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -188,6 +189,107 @@ func (ctl *PosController) LocationHealth(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Sales reads
|
||||
//
|
||||
// Unlike the ingest handlers above, these answer in the usual
|
||||
// `{code, message, status, details}` envelope — they are read by the web app,
|
||||
// not by a terminal, and nothing about them is bound to the till's contract.
|
||||
|
||||
// posSalesFilter reads the shared query parameters.
|
||||
func posSalesFilter(c *fiber.Ctx) (models.PosSalesFilter, error) {
|
||||
locationID, err := strconv.Atoi(strings.TrimSpace(c.Query("locationid")))
|
||||
if err != nil || locationID <= 0 {
|
||||
return models.PosSalesFilter{}, fmt.Errorf("locationid is required")
|
||||
}
|
||||
|
||||
pageno, _ := strconv.Atoi(c.Query("pageno", "0"))
|
||||
pagesize, _ := strconv.Atoi(c.Query("pagesize", "50"))
|
||||
|
||||
return models.PosSalesFilter{
|
||||
Locationid: locationID,
|
||||
Fromdate: strings.TrimSpace(c.Query("fromdate")),
|
||||
Todate: strings.TrimSpace(c.Query("todate")),
|
||||
Terminalid: strings.TrimSpace(c.Query("terminalid")),
|
||||
Cashiername: strings.TrimSpace(c.Query("cashiername")),
|
||||
Paymentmode: strings.TrimSpace(c.Query("paymentmode")),
|
||||
Pageno: pageno,
|
||||
Pagesize: pagesize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetSales lists counter bills for an outlet, newest first.
|
||||
func (ctl *PosController) GetSales(c *fiber.Ctx) error {
|
||||
filter, err := posSalesFilter(c)
|
||||
if err != nil {
|
||||
return posBadRequest(c, err)
|
||||
}
|
||||
|
||||
page, err := ctl.posService.Sales(filter)
|
||||
if err != nil {
|
||||
return posServerError(c, "GetSales", err)
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{"code": http.StatusOK, "status": true, "details": page})
|
||||
}
|
||||
|
||||
// GetSaleDetail returns one bill with its lines.
|
||||
//
|
||||
// Accepts the terminal's order UUID, the invoice number, or this backend's
|
||||
// posorderid — a support call starts from whichever the caller is looking at.
|
||||
func (ctl *PosController) GetSaleDetail(c *fiber.Ctx) error {
|
||||
locationID, err := strconv.Atoi(strings.TrimSpace(c.Query("locationid")))
|
||||
if err != nil || locationID <= 0 {
|
||||
return posBadRequest(c, fmt.Errorf("locationid is required"))
|
||||
}
|
||||
|
||||
reference := strings.TrimSpace(c.Query("reference"))
|
||||
if reference == "" {
|
||||
return posBadRequest(c, fmt.Errorf("reference is required — an order id, invoice number or posorderid"))
|
||||
}
|
||||
|
||||
bill, err := ctl.posService.SaleDetail(locationID, reference)
|
||||
if err != nil {
|
||||
return posServerError(c, "GetSaleDetail", err)
|
||||
}
|
||||
if bill == nil {
|
||||
return c.Status(http.StatusNotFound).JSON(fiber.Map{
|
||||
"code": http.StatusNotFound,
|
||||
"message": "no bill matches that reference at this outlet",
|
||||
"status": false,
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{"code": http.StatusOK, "status": true, "details": bill})
|
||||
}
|
||||
|
||||
// GetSalesSummary totals a range, split by tender, day and till.
|
||||
func (ctl *PosController) GetSalesSummary(c *fiber.Ctx) error {
|
||||
filter, err := posSalesFilter(c)
|
||||
if err != nil {
|
||||
return posBadRequest(c, err)
|
||||
}
|
||||
|
||||
summary, err := ctl.posService.SalesSummary(filter)
|
||||
if err != nil {
|
||||
return posServerError(c, "GetSalesSummary", err)
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{"code": http.StatusOK, "status": true, "details": summary})
|
||||
}
|
||||
|
||||
func posBadRequest(c *fiber.Ctx, err error) error {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
||||
"code": http.StatusBadRequest, "message": err.Error(), "status": false,
|
||||
})
|
||||
}
|
||||
|
||||
func posServerError(c *fiber.Ctx, op string, err error) error {
|
||||
log.Printf("pos %s: %v", op, err)
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
|
||||
"code": http.StatusInternalServerError, "message": err.Error(), "status": false,
|
||||
})
|
||||
}
|
||||
|
||||
// posIngestError decides whether the terminal should retry.
|
||||
//
|
||||
// The distinction matters more than the message does. A misconfigured store id
|
||||
|
||||
Reference in New Issue
Block a user