diff --git a/services/agents/platform.yaml b/services/agents/platform.yaml new file mode 100644 index 0000000..1aa0c39 --- /dev/null +++ b/services/agents/platform.yaml @@ -0,0 +1,32 @@ +# Nearle's own staff, in the platform workspace. +# +# One tool, and that is the point rather than a gap. A platform account carries +# no tenant — `issuperadmin` reads across every merchant and belongs to none — +# and every tool that reads a shop's data declares `RequiresTenant` or +# `RequiresBranch`, so all eight of them refuse a caller with no shop chosen. +# +# Giving this agent those tools anyway would produce a Buddy that accepts every +# question and answers "pick a shop first" to all of them, which reads as broken +# rather than as scoped. So it is handed the one tool that genuinely works +# without a tenant, and its prompt says plainly what it cannot do. +# +# When a platform account can act inside a chosen tenant, this agent gains the +# read tools and the prompt below loses its second paragraph. +name: platform +tier: fast + +system: | + You answer questions about how Nearle itself works, for Nearle's own staff + working across every merchant on the platform: onboarding a tenant, opening a + branch, how the global catalogue reaches a shop's own product list, what the + delivery partner assignment does. + + You cannot read any shop's data from here. A platform account is not scoped to + a merchant, and the tools that read orders, stock, tills and deliveries need + one. If you are asked what is stuck, what is selling or what needs approval, + say that those answers come from inside a merchant's own console and that you + cannot reach them from the platform workspace — do not guess, and do not offer + a figure from anywhere else. + +tools: + - help diff --git a/services/agents_test.go b/services/agents_test.go index 2a6262f..9f5ae0f 100644 --- a/services/agents_test.go +++ b/services/agents_test.go @@ -262,3 +262,43 @@ func TestAgentNamesAreSortedSoTwoDeploymentsLogTheSameLine(t *testing.T) { t.Fatalf("not sorted: %v", names) } } + +// The platform agent may only hold tools a caller with no tenant can run. +// +// Nearle's own staff are `issuperadmin`: they read across every merchant and +// belong to none, so `tools.Caller.Tenantid` is zero and `satisfies` refuses +// anything declaring RequiresTenant or RequiresBranch. An agent holding those +// would accept every question and answer "pick a shop first" to all of them, +// which reads as a broken assistant rather than a scoped one. +// +// Asserted rather than trusted, because adding a tool to a YAML file is one +// line and the failure it produces is invisible until somebody in the platform +// workspace asks a question. +func TestThePlatformAgentOnlyHoldsToolsThatNeedNoTenant(t *testing.T) { + registry := realRegistry(t) + + agents, err := LoadAgents("", registry.Has) + if err != nil { + t.Fatalf("agents: %v", err) + } + + platform, ok := agents["platform"] + if !ok { + t.Fatal("no platform agent — Nearle Admin has no assistant at all") + } + if len(platform.Tools) == 0 { + t.Fatal("the platform agent holds no tools") + } + + for _, name := range platform.Tools { + tool, ok := registry.Tool(name) + if !ok { + t.Fatalf("platform names %q, which is not in the registry", name) + } + if tool.Needs != tools.RequiresNothing { + t.Fatalf( + "platform holds %q, which needs a shop — a platform account has none, so every "+ + "question using it is refused", name) + } + } +} diff --git a/services/tools/help/assigning-a-partner.md b/services/tools/help/assigning-a-partner.md new file mode 100644 index 0000000..1fa5c12 --- /dev/null +++ b/services/tools/help/assigning-a-partner.md @@ -0,0 +1,31 @@ +--- +question: What does assigning a delivery partner to a merchant do? +also: + - how does a shop get riders + - merchant has no riders to assign + - change which partner supplies a shop + - what does partnerid zero mean +area: platform +source: src/api/tenants.ts, src/api/deliveries.ts +--- +A delivery partner is the company that supplies riders. Assigning one to a +merchant makes that partner's riders available on the merchant's assign screen, +alongside any riders the merchant hired itself. + +One partner routinely serves many merchants — this is not a one-to-one +relationship, and reassigning a merchant does not take the partner away from +anyone else. + +**Assigning no partner is a real setting, not an empty one.** A merchant with no +partner uses its own riders, and that is a supported way to run a shop rather +than a half-finished setup. A merchant with neither a partner nor its own riders +has nobody to assign deliveries to, and that is what to check first when a shop +reports an empty rider list. + +Only Nearle can set this. It is deliberately not something a merchant can edit +about themselves, because a merchant who could set it would be able to move +themselves under another partner's riders and billing. + +A partner works one district. If a merchant's branches sit in a district the +partner does not cover, the partner's riders will not appear there even though +the assignment exists. diff --git a/services/tools/help/districts.md b/services/tools/help/districts.md new file mode 100644 index 0000000..434bb99 --- /dev/null +++ b/services/tools/help/districts.md @@ -0,0 +1,27 @@ +--- +question: What is a district, and what happens when a partner is onboarded in a new one? +also: + - applocationid + - region not in the list + - riders not showing in a new city + - can I add a district +area: platform +source: repositories/partnerRepository.go +--- +A district is the delivery region a partner works and a rider is placed in. It +is not the same thing as a branch: a merchant has branches, a district covers +whichever of them sit inside it. + +Onboarding a partner into a district that is not open yet **opens it**. The +district is created along with the operating defaults every rider query needs, +copied from a district already running rather than invented. So a partner can be +set up in a city Nearle has not traded in before, without a separate step. + +This matters because a district that exists in name but has no operating +configuration hides every rider placed in it. Riders can be created, they appear +to save, and they are absent from every assign screen. The onboarding path +creates both halves together so that state cannot be reached from the console. + +A partner works one district. If a partner's riders are not appearing for a +merchant, check that the merchant's branches are in the same district as the +partner, before looking at the riders themselves. diff --git a/services/tools/help/onboarding-a-branch.md b/services/tools/help/onboarding-a-branch.md new file mode 100644 index 0000000..4219095 --- /dev/null +++ b/services/tools/help/onboarding-a-branch.md @@ -0,0 +1,32 @@ +--- +question: What does creating a branch actually create? +also: + - new outlet cannot sign in + - branch login has no password + - do I need a user before I add a branch + - onboarding a store what happens +area: platform +source: repositories/tenantRepository.go, src/auth/session.ts +--- +A branch is never created on its own. It arrives with somebody who can run it, +and there are two ways to give it one. + +Name an existing person and they are bound to the new outlet. They must already +belong to that same merchant and must not be a till account — a userid from +another business, or one that does not exist, is refused with the same message, +and the branch is not created at all rather than left standing with nobody. + +Give an email instead and a login is spawned from it. That account is named +after the shop, sits on the shop's email, and **is created with no password**. +That is deliberate, not a fault: the first person to sign in is told the account +needs a password and sets one, and only then can they get in. + +So a newly created branch whose operator reports they "cannot sign in" has +usually not been broken — they have not been through the password step yet. The +account is real and active; it simply has nothing to check a password against +until somebody sets one. + +If the branch was created with neither an operator nor an email, it was not +created. That combination is refused outright, because an outlet nobody can sign +in to still appears in every list and every branch picker, and the first person +to notice is whoever is standing in the shop. diff --git a/services/tools/help/platform-vs-merchant.md b/services/tools/help/platform-vs-merchant.md new file mode 100644 index 0000000..fcabe9a --- /dev/null +++ b/services/tools/help/platform-vs-merchant.md @@ -0,0 +1,26 @@ +--- +question: Why can I not see a shop's orders or stock from the platform workspace? +also: + - buddy will not answer about a merchant + - nearle admin cannot see sales + - how do I check what is stuck for a tenant + - platform account no tenant +area: platform +source: services/tools/registry.go, services/agents/platform.yaml +--- +A Nearle staff account reads across every merchant and belongs to none. It +carries no shop, and every tool that reads orders, stock, tills or deliveries +needs one — so from the platform workspace those questions have no answer rather +than a slow one. + +That is the design, not a missing feature. "Every merchant at once" is not an +answer to "what is stuck?", and a total across unrelated businesses would be a +number nobody could act on. + +To look at one merchant's operations, work inside that merchant's own console, +where the shop is fixed and the figures mean something. + +What can be answered from here is how Nearle itself works: what onboarding a +branch creates, what assigning a delivery partner does, how the global catalogue +reaches a shop's shelf, and why a till account is not a console login. If an +answer would need a specific shop's data, it will say so rather than estimate. diff --git a/services/tools/help_test.go b/services/tools/help_test.go index e2c2b01..89ede38 100644 --- a/services/tools/help_test.go +++ b/services/tools/help_test.go @@ -236,3 +236,83 @@ func TestHelpNeedsNoTenant(t *testing.T) { t.Fatal("staff got no answer") } } + +/* ── The platform passages ──────────────────────────────────────────────── */ + +// Nearle's own staff, whose questions are about the platform rather than about +// a shop. +// +// These exist because the `platform` agent holds ONE tool — `help` — and can +// hold no other: a staff account carries no tenant, so every tool that reads a +// shop's data refuses it. The corpus is therefore the entire capability of the +// assistant in the Nearle Admin workspace, and a passage that cannot be +// retrieved is a question that has no answer at all there. +func TestThePlatformQuestionsFindTheirOwnEntry(t *testing.T) { + for question, wantEntry := range map[string]string{ + "new outlet cannot sign in": "What does creating a branch actually create?", + "do I need a user before I add a branch": "What does creating a branch actually create?", + "how does a shop get riders": "What does assigning a delivery partner to a merchant do?", + "what does partnerid zero mean": "What does assigning a delivery partner to a merchant do?", + "riders not showing in a new city": "What is a district, and what happens when a partner is onboarded in a new one?", + "nearle admin cannot see sales": "Why can I not see a shop's orders or stock from the platform workspace?", + "how do I check what is stuck for a tenant": "Why can I not see a shop's orders or stock from the platform workspace?", + } { + answers := ask(t, question) + if len(answers) == 0 { + t.Fatalf("no answer for %q", question) + } + if answers[0].Question != wantEntry { + t.Fatalf("%q was answered with %q, wanted %q", question, answers[0].Question, wantEntry) + } + } +} + +func TestThePlatformPassagesDoNotSwallowTheMerchantOnes(t *testing.T) { + // Four passages were added at once, and they share vocabulary with the + // merchant ones — branches, riders, partners, shops. A new passage that + // outranks an existing answer breaks a question that used to work, silently, + // and the person sees a confident answer to something they did not ask. + for question, wantEntry := range map[string]string{ + "how do I add a cashier": "How do I add a cashier?", + "what happens when I assign a rider": "What happens when I assign a rider?", + "till login not working on the website": "What is the difference between a till account and a console login?", + "re-import from the global catalogue": "What does re-importing a product do?", + } { + answers := ask(t, question) + if len(answers) == 0 || answers[0].Question != wantEntry { + got := "nothing" + if len(answers) > 0 { + got = answers[0].Question + } + t.Fatalf("%q now answers with %q, wanted %q", question, got, wantEntry) + } + } +} + +func TestEveryChipOnThePlatformPagesHasAnAnswer(t *testing.T) { + // The console's own chips, verbatim from `assistantContext.ts`. + // + // The rule that file states: a page is only given an agent when that agent + // can answer every chip on it, because a chip coming back "I cannot look + // that up" reads as a broken assistant rather than an unbuilt feature. In + // the platform workspace the corpus IS the assistant, so a chip with no + // passage behind it is a dead button. + // + // Duplicated here rather than imported because the two repositories deploy + // separately: this is the half that can fail on its own. + for _, chip := range []string{ + "What does creating a branch actually create?", + "Why can I not see a shop's orders from here?", + "How does a shop get riders?", + "What is the difference between a till account and a console login?", + "New outlet cannot sign in", + "What is a district?", + "What does re-importing a product do?", + "Why is a product in my catalogue but not on the shelf?", + "Why can I not see a shop's stock from here?", + } { + if answers := ask(t, chip); len(answers) == 0 { + t.Fatalf("the chip %q has no passage behind it — it is a dead button", chip) + } + } +} diff --git a/services/tools/testdata/help_cashier.json b/services/tools/testdata/help_cashier.json index a6d17f7..ff28a49 100644 --- a/services/tools/testdata/help_cashier.json +++ b/services/tools/testdata/help_cashier.json @@ -12,9 +12,9 @@ "source": "src/api/people.ts, src/auth/session.ts" }, { - "question": "Why do online and counter sales not add up to one total?", - "answer": "App orders and counter bills are kept in two separate sets of books, and nothing\nreconciles them into a single figure.\n\nAn app order is placed by a customer and may carry a delivery. A counter bill is\nrung on a till in the shop. They are counted separately everywhere in the\nconsole, which is why a revenue figure from one place will not match a total\nfrom the other.\n\nWhen you need both, read them side by side and say which is which. Adding them\ntogether produces a number that looks authoritative and is not.", - "source": "src/features/store-admin/pages/SalesPage.tsx, services/posService.go" + "question": "What does creating a branch actually create?", + "answer": "A branch is never created on its own. It arrives with somebody who can run it,\nand there are two ways to give it one.\n\nName an existing person and they are bound to the new outlet. They must already\nbelong to that same merchant and must not be a till account — a userid from\nanother business, or one that does not exist, is refused with the same message,\nand the branch is not created at all rather than left standing with nobody.\n\nGive an email instead and a login is spawned from it. That account is named\nafter the shop, sits on the shop's email, and **is created with no password**.\nThat is deliberate, not a fault: the first person to sign in is told the account\nneeds a password and sets one, and only then can they get in.\n\nSo a newly created branch whose operator reports they \"cannot sign in\" has\nusually not been broken — they have not been through the password step yet. The\naccount is real and active; it simply has nothing to check a password against\nuntil somebody sets one.\n\nIf the branch was created with neither an operator nor an email, it was not\ncreated. That combination is refused outright, because an outlet nobody can sign\nin to still appears in every list and every branch picker, and the first person\nto notice is whoever is standing in the shop.", + "source": "repositories/tenantRepository.go, src/auth/session.ts" } ], "count": 3,