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

200 lines
6.5 KiB
Go

package tools
import (
"context"
"errors"
"testing"
"nearle/models"
)
type fakeBranches struct {
rows []models.Ordersummarylocation
err error
seen int
}
func (f *fakeBranches) GetLocationOrderSummary(tenantID int) ([]models.Ordersummarylocation, error) {
f.seen = tenantID
return f.rows, f.err
}
func runBranches(t *testing.T, rows []models.Ordersummarylocation, caller Caller) (Result, *fakeBranches) {
t.Helper()
branches := &fakeBranches{rows: rows}
r := New(nil)
if err := r.Register(BranchPerformance(branches)); err != nil {
t.Fatalf("registering: %v", err)
}
result, err := r.Call(context.Background(), Agent{Name: "orders", Tools: []string{"branch_performance"}},
"branch_performance", nil, caller)
if err != nil {
t.Fatalf("calling: %v", err)
}
return result, branches
}
func branchRows(t *testing.T, result Result) []BranchRow {
t.Helper()
rows, ok := result.Rows.([]BranchRow)
if !ok {
t.Fatalf("rows are not branches: %T", result.Rows)
}
return rows
}
func branch(id int, name string, total, delivered, cancelled int) models.Ordersummarylocation {
return models.Ordersummarylocation{
Locationid: id, Locationname: name,
Total: total, Delivered: delivered, Cancelled: cancelled,
}
}
/* ── The tenant is the session's ───────────────────────────────────────── */
func TestBranchPerformanceReadsTheSessionsTenant(t *testing.T) {
_, branches := runBranches(t, nil, Caller{Userid: 904, Tenantid: 1147})
if branches.seen != 1147 {
t.Fatalf("read tenant %d", branches.seen)
}
}
/* ── The arithmetic ────────────────────────────────────────────────────── */
func TestTheCancelRateIsComputedHereNotByTheModel(t *testing.T) {
// A model asked to divide two numbers in a sentence usually gets it right
// and sometimes does not, and the answer looks the same either way.
result, _ := runBranches(t, []models.Ordersummarylocation{branch(1, "R Mart", 200, 150, 30)}, merchantCaller)
row := branchRows(t, result)[0]
if row.CancelRate != 15 {
t.Fatalf("30 of 200 reported as %.1f%%", row.CancelRate)
}
if row.DeliveryRate != 75 {
t.Fatalf("150 of 200 reported as %.1f%%", row.DeliveryRate)
}
if row.Outstanding != 20 {
t.Fatalf("outstanding is %d, not 20", row.Outstanding)
}
}
func TestABranchWithNoOrdersHasNoRate(t *testing.T) {
// 0% would read as "nothing is cancelled here", which is a claim about
// performance rather than the absence of any.
result, _ := runBranches(t, []models.Ordersummarylocation{branch(2, "New Shop", 0, 0, 0)}, merchantCaller)
row := branchRows(t, result)[0]
if row.CancelRate != 0 || row.Orders != 0 {
t.Fatalf("an empty branch got a rate: %+v", row)
}
if row.Note == "" {
t.Fatal("an empty branch does not say why it has no numbers")
}
}
/* ── Standing out is relative ──────────────────────────────────────────── */
func TestABranchIsJudgedAgainstItsOwnBusinessNotAFixedNumber(t *testing.T) {
// 9% is poor in a business averaging 3% and unremarkable in one averaging
// 11%. A fixed threshold flags every branch of the second or none of the
// first.
tight, _ := runBranches(t, []models.Ordersummarylocation{
branch(1, "Good", 100, 97, 2),
branch(2, "Bad", 100, 88, 9),
}, merchantCaller)
loose, _ := runBranches(t, []models.Ordersummarylocation{
branch(1, "One", 100, 85, 12),
branch(2, "Two", 100, 87, 9),
}, merchantCaller)
flaggedTight := 0
for _, row := range branchRows(t, tight) {
if row.Note != "" {
flaggedTight++
}
}
flaggedLoose := 0
for _, row := range branchRows(t, loose) {
if row.Note != "" {
flaggedLoose++
}
}
if flaggedTight == 0 {
t.Fatal("9% against a 5.5% average was not worth mentioning")
}
if flaggedLoose > 0 {
t.Fatal("9% against a 10.5% average was flagged as standing out")
}
}
func TestTheWorstBranchIsFirst(t *testing.T) {
result, _ := runBranches(t, []models.Ordersummarylocation{
branch(1, "Fine", 100, 95, 2),
branch(2, "Poor", 100, 80, 18),
branch(3, "Middling", 100, 90, 7),
}, merchantCaller)
if got := branchRows(t, result)[0].Branch; got != "Poor" {
t.Fatalf("the worst branch is not first: %q", got)
}
}
func TestTheBusinessAverageIsStatedForTheModelToCompareAgainst(t *testing.T) {
// Without it a model reports "15% cancelled" with no idea whether that is
// alarming or ordinary for this shop.
result, _ := runBranches(t, []models.Ordersummarylocation{
branch(1, "One", 100, 90, 10),
branch(2, "Two", 100, 80, 20),
}, merchantCaller)
if result.Note == "" {
t.Fatal("the answer carries no business-wide figure")
}
}
/* ── Failure ───────────────────────────────────────────────────────────── */
func TestABrokenBranchReadIsAnErrorNotAnEmptyShop(t *testing.T) {
// An empty list would be reported as "you have no branches", which is a
// statement about the business rather than about the database.
branches := &fakeBranches{err: errors.New("the database is down")}
r := New(nil)
_ = r.Register(BranchPerformance(branches))
_, err := r.Call(context.Background(), Agent{Name: "orders", Tools: []string{"branch_performance"}},
"branch_performance", nil, merchantCaller)
if err == nil {
t.Fatal("a failed read was reported as a successful answer")
}
}
func TestStaffMustPickATenantForBranchPerformance(t *testing.T) {
branches := &fakeBranches{}
r := New(nil)
_ = r.Register(BranchPerformance(branches))
_, err := r.Call(context.Background(), Agent{Name: "orders", Tools: []string{"branch_performance"}},
"branch_performance", nil, Caller{Userid: 12, Superadmin: true})
if err == nil {
t.Fatal("staff compared branches across every tenant at once")
}
}
/* ── percent ───────────────────────────────────────────────────────────── */
func TestPercentGuardsTheZeroDenominator(t *testing.T) {
// NaN serialises as null, which a model reads as "no data" rather than
// "no orders".
if got := percent(0, 0); got != 0 {
t.Fatalf("0 of 0 is %v", got)
}
if got := percent(1, 3); got != 33.3 {
t.Fatalf("1 of 3 rounded to %v", got)
}
if got := percent(2, 3); got != 66.7 {
t.Fatalf("2 of 3 rounded to %v", got)
}
}