Files
backend_fiesta/services/tools/channels.go
2026-09-23 17:26:13 +05:30

132 lines
4.3 KiB
Go

package tools
import (
"context"
"fmt"
"time"
"nearle/models"
)
// "Compare online and counter sales."
//
// Two ledgers, not two columns of one. App orders live in `orders`; counter
// bills arrive from the tills and live in the POS tables. Nothing reconciles
// them, and the console's own Sales page says so in as many words — so this
// tool reports them side by side and states plainly that they are separate
// books. A single "total sales" figure invented by adding them would be the
// most useful-looking wrong number on the page.
//
// ── The model never sends a date ────────────────────────────────────────────
//
// It sends a number of days, and the range is computed here. Dates are the
// thing models get wrong most reliably — a format, a timezone, an off-by-one on
// "last week" — and every one of those errors produces a plausible figure for
// the wrong period, which is undetectable in a sentence.
// RevenueReader is the app-order side.
type RevenueReader interface {
GetRevenueSummary(tid, lid int, fdate, tdate string) (*models.TenantRevenueSummary, error)
}
// CounterReader is the till side.
type CounterReader interface {
SalesSummary(f models.PosSalesFilter) (*models.PosSalesSummary, error)
}
// ChannelTotals is one channel's takings over the period.
type ChannelTotals struct {
Channel string `json:"channel"`
Revenue float64 `json:"revenue"`
// Bills only exist on the counter side; app orders are counted by the
// revenue read, which returns no order count. Omitted rather than reported
// as 0, which would read as "no orders".
Bills int `json:"bills,omitempty"`
Ledger string `json:"ledger"`
Caveat string `json:"caveat,omitempty"`
}
// defaultChannelDays is the period when nobody says otherwise.
//
// Seven, so the answer covers a full trading week including the weekend. A
// single day makes every Monday look like a collapse.
const defaultChannelDays = 7
// SalesByChannel builds the tool.
func SalesByChannel(orders RevenueReader, counter CounterReader, now func() time.Time) Tool {
if now == nil {
now = time.Now
}
return Tool{
Name: "sales_by_channel",
Description: "App order revenue and counter (till) takings side by side for one branch, over a recent period. " +
"Use for questions comparing online and counter sales, or asking which channel is bigger. " +
"The two come from separate ledgers and are never added together.",
Needs: RequiresBranch,
Scope: ScopeRead,
Schema: Schema{Fields: []Field{{
Name: "days",
Description: "How many days back to cover, ending today. Defaults to 7.",
Kind: KindInt,
Min: 1,
Max: 365,
Default: defaultChannelDays,
}}},
Handler: func(_ context.Context, req Request) (Result, error) {
days := req.Int("days")
if days <= 0 {
days = defaultChannelDays
}
to := now()
from := to.AddDate(0, 0, -(days - 1))
fromDate, toDate := from.Format("2006-01-02"), to.Format("2006-01-02")
rows := make([]ChannelTotals, 0, 2)
revenue, err := orders.GetRevenueSummary(req.Caller.Tenantid, req.Caller.Locationid, fromDate, toDate)
if err != nil {
return Result{}, err
}
if revenue != nil {
rows = append(rows, ChannelTotals{
Channel: "App orders",
Revenue: revenue.OverallRevenue,
Ledger: "orders",
})
}
bills, err := counter.SalesSummary(models.PosSalesFilter{
Locationid: req.Caller.Locationid,
Fromdate: fromDate,
Todate: toDate,
})
if err != nil {
return Result{}, err
}
if bills != nil {
rows = append(rows, ChannelTotals{
Channel: "Counter",
Revenue: bills.Grosssales,
Bills: bills.Billcount,
Ledger: "pos",
// Gross, so it is not comparable like for like with the app
// figure without saying so. Better stated on the row than
// left for a reader to assume either way.
Caveat: "Gross, before discount and round-off.",
})
}
return Result{
Rows: rows,
Count: len(rows),
Source: "/admin/sales",
Scope: "this branch",
Note: fmt.Sprintf(
"%s to %s. These are two separate ledgers — report them side by side and do not add them into one total.",
fromDate, toDate),
}, nil
},
}
}