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