agent
This commit is contained in:
124
services/tools/approvestock.go
Normal file
124
services/tools/approvestock.go
Normal 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
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user