This commit is contained in:
2026-09-23 17:26:13 +05:30
parent 8e1549764b
commit 697b77f8c1
54 changed files with 5750 additions and 69 deletions

View File

@@ -0,0 +1,124 @@
package tools
import (
"context"
"fmt"
"strconv"
"nearle/models"
)
// "Approve that stock request" — the first thing Buddy can change.
//
// Chosen to be first because it is the smallest honest write in the product: one
// column, one row, reversible by a human in the same screen, and it pairs with a
// read the assistant already has. Somebody asks what is waiting, Buddy lists it,
// they say approve the one for rice, and a card appears naming that request.
//
// ── Ownership is re-derived, never taken from the argument ──────────────────
//
// The model supplies a request id and nothing else. Both halves look that id up
// among THIS tenant's pending requests and refuse if it is not there — so an id
// belonging to another merchant is not found rather than approved, and the check
// is a lookup rather than a comparison somebody could forget to make.
// ApprovalWriter is the write half. Split from ApprovalReader so a tool that
// only lists cannot be handed the ability to change anything.
type ApprovalWriter interface {
UpdateStockRequest(requestID int, status string) error
}
// stockApprovedStatus is the word the column expects.
//
// Capitalised, matching `Pending`, because the repository compares it as given.
// A lowercase "approved" would write a status nothing else recognises — the
// request would leave the pending list and arrive nowhere.
const stockApprovedStatus = "Approved"
// ApproveStockRequest builds the tool.
func ApproveStockRequest(requests ApprovalReader, writer ApprovalWriter) Tool {
find := func(tenantid, locationid, requestid int) (models.StockRequest, error) {
rows, err := requests.GetStockRequests(tenantid, locationid, "Pending", "", 1, 500)
if err != nil {
return models.StockRequest{}, err
}
for _, row := range rows {
if row.Requestid == requestid {
return row, nil
}
}
// One message for "does not exist", "belongs to another shop" and
// "somebody already approved it". Deliberately: the first two must not
// be distinguishable, or this becomes a way to ask whether a given id
// exists somewhere else on the platform.
return models.StockRequest{}, fmt.Errorf(
"stock request %d is not waiting for approval in this shop — it may already have been approved", requestid)
}
return WriteTool(Tool{
Name: "approve_stock_request",
Description: "Approve one stock request that a branch is waiting on, so the stock reaches their shelf. " +
"Use when somebody asks to approve, allow or release a specific request. " +
"This does not happen immediately — it produces a confirmation for the person to check first.",
Schema: Schema{Fields: []Field{{
Name: "requestid",
Description: "The id of the request to approve, from the pending approvals list.",
Kind: KindInt,
Required: true,
Min: 1,
}}},
},
// Propose: resolve the id into something a person can check, and refuse
// now rather than after they have pressed the button.
func(_ context.Context, req Request) (Proposal, error) {
id := req.Int("requestid")
row, err := find(req.Caller.Tenantid, req.Caller.Locationid, id)
if err != nil {
return Proposal{}, err
}
proposal := Proposal{
Summary: fmt.Sprintf("Approve %d × %s for %s", row.Qty, row.Productname, row.Locationname),
Details: []ProposalDetail{
// Id and name together. An id alone cannot be checked by a
// person; a name alone is ambiguous when two branches ask
// for the same product.
{Label: "Request", Value: "#" + strconv.Itoa(row.Requestid)},
{Label: "Product", Value: row.Productname},
{Label: "Quantity", Value: strconv.Itoa(row.Qty)},
{Label: "Branch", Value: row.Locationname},
{Label: "Requested", Value: row.Created.Format("2 January 2006")},
},
// The resolved id, sealed into the card. The model's arguments
// are not carried forward — only what this lookup confirmed.
args: map[string]any{"requestid": row.Requestid},
}
if row.Qty > 500 {
proposal.Warning = "That is a large quantity. Check it is what the branch meant to ask for."
}
return proposal, nil
},
// Execute: everything checked at proposal time may have changed while
// the card was on screen. Somebody else may have approved it; the branch
// may have withdrawn it.
func(_ context.Context, req Request) (Result, error) {
id := req.Int("requestid")
row, err := find(req.Caller.Tenantid, req.Caller.Locationid, id)
if err != nil {
return Result{}, err
}
if err := writer.UpdateStockRequest(row.Requestid, stockApprovedStatus); err != nil {
return Result{}, err
}
return Result{
Rows: []ProposalDetail{
{Label: "Approved", Value: fmt.Sprintf("%d × %s for %s", row.Qty, row.Productname, row.Locationname)},
},
Count: 1,
Source: "/admin/inventory",
Scope: scopeWords(req.Caller),
Note: "Done. The branch can now receive this stock.",
}, nil
})
}