125 lines
4.9 KiB
Go
125 lines
4.9 KiB
Go
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
|
||
})
|
||
}
|