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