@kivimedia/kmhub 2.9.1 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +170 -170
- package/bin/kmhub.mjs +896 -896
- package/coach-book-output-guard.mjs +760 -760
- package/index.mjs +57 -57
- package/package.json +56 -56
- package/prompts/briefing.md +29 -29
- package/prompts/luxury.md +70 -70
- package/prompts/play.md +49 -49
- package/prompts/run.md +37 -36
- package/prompts/setup.md +33 -33
- package/prompts/vs-booked.md +46 -46
- package/prompts/what-can-you-do.md +40 -40
- package/prompts.mjs +110 -110
- package/read-only-tools.json +143 -142
- package/remote.mjs +929 -929
- package/tools/balloon-costing.mjs +80 -80
- package/tools/booking-equipment.mjs +110 -110
- package/tools/bridges.mjs +54 -54
- package/tools/briefing.mjs +91 -91
- package/tools/calendar.mjs +170 -170
- package/tools/capabilities.mjs +155 -155
- package/tools/catalog.mjs +288 -288
- package/tools/clubs.mjs +176 -176
- package/tools/coach.mjs +771 -771
- package/tools/compare.mjs +76 -76
- package/tools/core.mjs +244 -244
- package/tools/crm.mjs +209 -209
- package/tools/dubsado.mjs +137 -137
- package/tools/exports.mjs +128 -128
- package/tools/fact-review.mjs +125 -125
- package/tools/flows.mjs +261 -261
- package/tools/forms.mjs +158 -158
- package/tools/gols.mjs +144 -134
- package/tools/hr.mjs +162 -162
- package/tools/knowledge.mjs +129 -125
- package/tools/marketing.mjs +396 -396
- package/tools/meta.mjs +245 -245
- package/tools/military.mjs +244 -244
- package/tools/money.mjs +235 -197
- package/tools/outreach.mjs +238 -238
- package/tools/pending.mjs +122 -122
- package/tools/photos.mjs +140 -140
- package/tools/plays.mjs +244 -244
- package/tools/profile.mjs +118 -118
- package/tools/radar.mjs +173 -173
- package/tools/recurring-invoices.mjs +149 -149
- package/tools/reengage.mjs +434 -434
- package/tools/schedules.mjs +55 -55
- package/tools/setup.mjs +168 -168
- package/tools/sops-bridges.mjs +86 -86
- package/tools/sops.mjs +314 -314
- package/tools/sourcing.mjs +268 -268
- package/tools/strategy.mjs +146 -146
- package/tools/studio.mjs +132 -132
- package/tools/venueradar.mjs +151 -151
- package/tools/voice.mjs +137 -134
- package/tools.mjs +407 -407
package/tools/money.mjs
CHANGED
|
@@ -1,197 +1,235 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tool family: money - invoices, quotes, proposals, contracts and payments.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* Money is the one area where a confident machine is most expensive to be wrong
|
|
6
|
-
* in, so this family answers questions and a person still issues the money:
|
|
7
|
-
*
|
|
8
|
-
* - There is no tool here that creates an invoice or a quote. An invoice row in
|
|
9
|
-
* KM Hub is born with a public pay link (invoices.public_id has a default,
|
|
10
|
-
* and the public checkout endpoint charges against it without ever looking at
|
|
11
|
-
* the invoice status), so a "draft" invoice is not inert the way an outreach
|
|
12
|
-
* draft is. The same is true of a Quote Builder quote, which is acceptable
|
|
13
|
-
* from an anonymous page the moment it exists.
|
|
14
|
-
* - There is no tool here that sends, reminds, voids, refunds or marks anything
|
|
15
|
-
* paid. Nothing in this family changes a number.
|
|
16
|
-
*
|
|
17
|
-
* The single most useful tool is km_money_now: one call, one readable answer.
|
|
18
|
-
* Everything else exists for the follow-up question.
|
|
19
|
-
*
|
|
20
|
-
* The family contract this file follows is documented in ./README.md.
|
|
21
|
-
*/
|
|
22
|
-
import { z } from 'zod';
|
|
23
|
-
|
|
24
|
-
export const FAMILY = 'money';
|
|
25
|
-
|
|
26
|
-
export const TOOLS = [
|
|
27
|
-
'km_money_now',
|
|
28
|
-
'km_list_invoices',
|
|
29
|
-
'km_get_invoice',
|
|
30
|
-
'km_list_quotes',
|
|
31
|
-
'km_get_quote',
|
|
32
|
-
'km_list_proposals',
|
|
33
|
-
'km_get_proposal',
|
|
34
|
-
'km_list_contracts',
|
|
35
|
-
'km_list_payments',
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
* @param {
|
|
51
|
-
* @param {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
'
|
|
68
|
-
|
|
69
|
-
'Reach for
|
|
70
|
-
|
|
71
|
-
' ' +
|
|
72
|
-
READ_ONLY,
|
|
73
|
-
{
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
},
|
|
100
|
-
async ({
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
'
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
out
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
},
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
'
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Tool family: money - invoices, quotes, proposals, contracts and payments.
|
|
3
|
+
*
|
|
4
|
+
* Ten tools, all of them read-only, and that is the whole point of this family.
|
|
5
|
+
* Money is the one area where a confident machine is most expensive to be wrong
|
|
6
|
+
* in, so this family answers questions and a person still issues the money:
|
|
7
|
+
*
|
|
8
|
+
* - There is no tool here that creates an invoice or a quote. An invoice row in
|
|
9
|
+
* KM Hub is born with a public pay link (invoices.public_id has a default,
|
|
10
|
+
* and the public checkout endpoint charges against it without ever looking at
|
|
11
|
+
* the invoice status), so a "draft" invoice is not inert the way an outreach
|
|
12
|
+
* draft is. The same is true of a Quote Builder quote, which is acceptable
|
|
13
|
+
* from an anonymous page the moment it exists.
|
|
14
|
+
* - There is no tool here that sends, reminds, voids, refunds or marks anything
|
|
15
|
+
* paid. Nothing in this family changes a number.
|
|
16
|
+
*
|
|
17
|
+
* The single most useful tool is km_money_now: one call, one readable answer.
|
|
18
|
+
* Everything else exists for the follow-up question.
|
|
19
|
+
*
|
|
20
|
+
* The family contract this file follows is documented in ./README.md.
|
|
21
|
+
*/
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
|
|
24
|
+
export const FAMILY = 'money';
|
|
25
|
+
|
|
26
|
+
export const TOOLS = [
|
|
27
|
+
'km_money_now',
|
|
28
|
+
'km_list_invoices',
|
|
29
|
+
'km_get_invoice',
|
|
30
|
+
'km_list_quotes',
|
|
31
|
+
'km_get_quote',
|
|
32
|
+
'km_list_proposals',
|
|
33
|
+
'km_get_proposal',
|
|
34
|
+
'km_list_contracts',
|
|
35
|
+
'km_list_payments',
|
|
36
|
+
'km_price_trend',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
// The money profile exists for callers who only want this side of the workspace.
|
|
40
|
+
export const PROFILES = ['money'];
|
|
41
|
+
|
|
42
|
+
/** Shared tail on every description, so the model never has to guess the limit. */
|
|
43
|
+
const READ_ONLY = 'This tool only reads. It cannot change a number, issue anything, or send anything to a client.';
|
|
44
|
+
|
|
45
|
+
/** Why there is no create tool, said in the two places a model is most likely to reach for one. */
|
|
46
|
+
const NO_ISSUING =
|
|
47
|
+
'There is deliberately no way to raise an invoice or a quote from here. In KM Hub a person does that, because an invoice becomes payable by the client the moment it exists. If the user asks you to create one, say plainly that they need to do it in KM Hub, and offer to pull up whatever they need to write it.';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {{ tool: Function }} server guarded registrar (see ./README.md)
|
|
51
|
+
* @param {(method: string, path: string, body?: any) => Promise<{ok:boolean,status:number,data:any}>} call
|
|
52
|
+
* @param {{ out: Function, qs: Function }} helpers shared response helpers from ../tools.mjs
|
|
53
|
+
*/
|
|
54
|
+
/** A route this KM Hub does not have yet, as opposed to an answer. */
|
|
55
|
+
function routeMissing(r) {
|
|
56
|
+
if (!(r.status === 404 || r.status === 405 || r.status === 501)) return false;
|
|
57
|
+
const err = r.data && typeof r.data === 'object' ? r.data.error : undefined;
|
|
58
|
+
return err !== 'not_found' || /Unknown route/i.test(String(r.data?.message || ''));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const PRICE_NOT_SUPPORTED =
|
|
62
|
+
'Price history is not available on this KM Hub yet. Nothing is wrong with the workspace and nothing was changed. ' +
|
|
63
|
+
'The owner can see prices in the web app at https://hub.kivimedia.co under Reports, Business Reporting, Prices, once it is there.';
|
|
64
|
+
|
|
65
|
+
export function register(server, call, { out, qs, text }) {
|
|
66
|
+
server.tool(
|
|
67
|
+
'km_money_now',
|
|
68
|
+
'The one money check: where this business stands right now, in a single call. It comes back with a headline sentence you can read straight out loud, plus the detail behind it: how much clients owe, how much of that is already overdue and which clients are late, how many contracts are still waiting on a signature, how many quotes and proposals are out with no answer yet, and how much money has actually landed this month and this year. ' +
|
|
69
|
+
'Reach for this first when the SUBJECT IS MONEY: how are we doing, what is outstanding, who owes me, what is late on the money side, do I need to chase anyone, how was this month. Only go to the other money tools afterwards, when the user wants the detail behind one line of it. ' +
|
|
70
|
+
'It is not the tool for the broad question about the day. If the user says "what needs me today", "catch me up", "what should I do first", "anything urgent" or "what did I miss", reach for km_briefing instead: money is one column of somebody\'s day, and km_briefing ranks the late invoices against the new enquiries, the unanswered replies, the overdue tasks and today\'s gig, none of which this tool can see. Answering the whole day from this one would report the money and silently drop the rest. ' +
|
|
71
|
+
'It tells you honestly when a number is incomplete, for instance when a workspace has more unpaid invoices than one read can cover, or when it bills in more than one currency. Invoices still sitting at draft are reported separately and are never counted as money owed, because they have not gone to anyone. ' +
|
|
72
|
+
READ_ONLY,
|
|
73
|
+
{},
|
|
74
|
+
async () => out(await call('GET', '/money/summary')),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
server.tool(
|
|
78
|
+
'km_list_invoices',
|
|
79
|
+
'List invoices, soonest due first. Each one comes back with the amount still owed, the due date, how many days late it is if it is late, and the client and event it belongs to. ' +
|
|
80
|
+
'Use paid: false for what is still owed, which is what people usually mean by "my invoices". Use overdue: true for the chase list. Use client_id to see one client. Use from and to for a due-date window, for instance everything due this month. ' +
|
|
81
|
+
'Reach for it when someone asks who owes money, what is outstanding, what is late, or wants to look through a particular client\'s invoices. If the question is broader than that ("how am I doing"), use km_money_now instead, which answers it in one call. ' +
|
|
82
|
+
NO_ISSUING +
|
|
83
|
+
' ' +
|
|
84
|
+
READ_ONLY,
|
|
85
|
+
{
|
|
86
|
+
limit: z.number().int().min(1).max(100).optional().describe('How many to return. Default 25.'),
|
|
87
|
+
paid: z
|
|
88
|
+
.boolean()
|
|
89
|
+
.optional()
|
|
90
|
+
.describe('false for invoices with money still owed on them. true for ones that are fully settled. Leave it out for both.'),
|
|
91
|
+
overdue: z.boolean().optional().describe('true for invoices that are past their due date and still owe money.'),
|
|
92
|
+
status: z
|
|
93
|
+
.string()
|
|
94
|
+
.optional()
|
|
95
|
+
.describe('One KM Hub status, e.g. draft, sent, viewed, partially_paid, paid, voided. Usually paid: false is the better filter.'),
|
|
96
|
+
client_id: z.string().optional().describe('UUID of a client, from km_find_contact.'),
|
|
97
|
+
from: z.string().optional().describe('Earliest DUE date, YYYY-MM-DD.'),
|
|
98
|
+
to: z.string().optional().describe('Latest DUE date, YYYY-MM-DD.'),
|
|
99
|
+
},
|
|
100
|
+
async ({ limit, paid, overdue, status, client_id, from, to }) =>
|
|
101
|
+
out(await call('GET', `/invoices${qs({ limit: limit ?? 25, paid, overdue, status, client_id, from, to })}`)),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
server.tool(
|
|
105
|
+
'km_get_invoice',
|
|
106
|
+
'Open one invoice in full: every line on it, every payment already recorded against it, and its instalment plan if it has one, with how late each instalment is. ' +
|
|
107
|
+
'Use it after km_list_invoices when someone asks what is actually on an invoice, how much of it has been paid already, or when the next instalment falls due. ' +
|
|
108
|
+
READ_ONLY,
|
|
109
|
+
{
|
|
110
|
+
invoice_id: z.string().describe('UUID of the invoice, from km_list_invoices or km_money_now.'),
|
|
111
|
+
},
|
|
112
|
+
async ({ invoice_id }) => out(await call('GET', `/invoices/${encodeURIComponent(String(invoice_id || '').trim())}`)),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
server.tool(
|
|
116
|
+
'km_list_quotes',
|
|
117
|
+
'List the quotes this business has put out. KM Hub keeps quotes in two places, the older CRM quotes and the newer Quote Builder, and this shows BOTH, each row tagged with a source field saying which one it came from. That matters: asking only one of them would tell the user they have no quotes when they have hundreds. ' +
|
|
118
|
+
'Filter by status to see what is still sitting with a client unanswered. Reach for it when someone asks what quotes are out, what has gone quiet, or what a client was quoted. ' +
|
|
119
|
+
NO_ISSUING +
|
|
120
|
+
' ' +
|
|
121
|
+
READ_ONLY,
|
|
122
|
+
{
|
|
123
|
+
limit: z.number().int().min(1).max(100).optional().describe('How many to return. Default 25.'),
|
|
124
|
+
status: z
|
|
125
|
+
.string()
|
|
126
|
+
.optional()
|
|
127
|
+
.describe('One status, e.g. draft, sent, viewed, accepted. The older quotes use rejected, the Quote Builder uses declined.'),
|
|
128
|
+
source: z
|
|
129
|
+
.enum(['crm', 'builder'])
|
|
130
|
+
.optional()
|
|
131
|
+
.describe('Narrow to one of the two quote systems. Leave it out to see both, which is almost always what you want.'),
|
|
132
|
+
},
|
|
133
|
+
async ({ limit, status, source }) => out(await call('GET', `/quotes${qs({ limit: limit ?? 25, status, source })}`)),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
server.tool(
|
|
137
|
+
'km_get_quote',
|
|
138
|
+
'Open one quote in full, with every priced line on it, so you can say what it actually includes and how it adds up. Works for a quote from either of the two quote systems. Use it after km_list_quotes. ' +
|
|
139
|
+
READ_ONLY,
|
|
140
|
+
{
|
|
141
|
+
quote_id: z.string().describe('UUID of the quote, from km_list_quotes.'),
|
|
142
|
+
},
|
|
143
|
+
async ({ quote_id }) => out(await call('GET', `/quotes/${encodeURIComponent(String(quote_id || '').trim())}`)),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
server.tool(
|
|
147
|
+
'km_list_proposals',
|
|
148
|
+
'List proposals and how far each one has got: sent, seen by the client, accepted, completed or declined, with the date of each step and the client and event attached. Archived ones are left out unless you ask for them. ' +
|
|
149
|
+
'Reach for it when someone asks what is out with clients, which proposals have gone quiet, or what is still waiting on an answer before they can plan the diary. ' +
|
|
150
|
+
READ_ONLY,
|
|
151
|
+
{
|
|
152
|
+
limit: z.number().int().min(1).max(100).optional().describe('How many to return. Default 25.'),
|
|
153
|
+
status: z
|
|
154
|
+
.string()
|
|
155
|
+
.optional()
|
|
156
|
+
.describe('One status, e.g. draft, published, sent, viewed, accepted, completed, declined.'),
|
|
157
|
+
client_id: z.string().optional().describe('UUID of a client, from km_find_contact.'),
|
|
158
|
+
include_archived: z.boolean().optional().describe('true to also show archived proposals. Default false.'),
|
|
159
|
+
},
|
|
160
|
+
async ({ limit, status, client_id, include_archived }) =>
|
|
161
|
+
out(await call('GET', `/proposals${qs({ limit: limit ?? 25, status, client_id, include_archived })}`)),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
server.tool(
|
|
165
|
+
'km_get_proposal',
|
|
166
|
+
'Open one proposal: its client, its event, the full trail of when it was sent, first seen, last seen, accepted or declined, and the won or lost figure if anyone recorded one against it. Use it after km_list_proposals when the question is about a specific proposal. ' +
|
|
167
|
+
READ_ONLY,
|
|
168
|
+
{
|
|
169
|
+
proposal_id: z.string().describe('UUID of the proposal, from km_list_proposals.'),
|
|
170
|
+
},
|
|
171
|
+
async ({ proposal_id }) => out(await call('GET', `/proposals/${encodeURIComponent(String(proposal_id || '').trim())}`)),
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
server.tool(
|
|
175
|
+
'km_list_contracts',
|
|
176
|
+
'List contracts and where each one stands on signing: whether it has gone out, whether the client has opened it, whether it is signed, who signed it and when, and how many days an unsigned one has been sitting with them. ' +
|
|
177
|
+
'Use signed: false for the chase list, which is the usual question before a gig. Reach for it when someone asks what is unsigned, whether a particular client has signed yet, or what still has to be tied down before an event. ' +
|
|
178
|
+
READ_ONLY,
|
|
179
|
+
{
|
|
180
|
+
limit: z.number().int().min(1).max(100).optional().describe('How many to return. Default 25.'),
|
|
181
|
+
status: z.string().optional().describe('One status, e.g. draft, sent, viewed, signed, voided.'),
|
|
182
|
+
signed: z.boolean().optional().describe('false for contracts still waiting on a signature. true for ones already signed.'),
|
|
183
|
+
},
|
|
184
|
+
async ({ limit, status, signed }) => out(await call('GET', `/contracts${qs({ limit: limit ?? 25, status, signed })}`)),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
server.tool(
|
|
188
|
+
'km_list_payments',
|
|
189
|
+
'List money that has actually come in, most recent first, with the method it arrived by, the reference on it, and the invoice or booking it was against. Refunds are included and marked as such, so the picture is honest rather than flattering. By default it shows only what KM Hub counts as real settled money; set include_pending to true to also see attempts that never settled. ' +
|
|
190
|
+
'Reach for it when someone asks what has been paid lately, whether a specific payment landed, or wants to tie the workspace up against a bank statement. For a single headline figure for the month, km_money_now is the better call. ' +
|
|
191
|
+
READ_ONLY,
|
|
192
|
+
{
|
|
193
|
+
limit: z.number().int().min(1).max(100).optional().describe('How many to return. Default 25.'),
|
|
194
|
+
kind: z.enum(['payment', 'refund', 'all']).optional().describe('Default all, which shows payments and refunds together.'),
|
|
195
|
+
include_pending: z
|
|
196
|
+
.boolean()
|
|
197
|
+
.optional()
|
|
198
|
+
.describe('true to also include payments that have not settled. Default false, so only real money is shown.'),
|
|
199
|
+
method: z
|
|
200
|
+
.string()
|
|
201
|
+
.optional()
|
|
202
|
+
.describe('One method, e.g. cash, check, credit_card, bank_transfer, venmo, paypal, zelle, stripe, other.'),
|
|
203
|
+
from: z.string().optional().describe('Earliest date the money was received, YYYY-MM-DD.'),
|
|
204
|
+
to: z.string().optional().describe('Latest date the money was received, YYYY-MM-DD.'),
|
|
205
|
+
},
|
|
206
|
+
async ({ limit, kind, include_pending, method, from, to }) =>
|
|
207
|
+
out(await call('GET', `/payments${qs({ limit: limit ?? 25, kind, include_pending, method, from, to })}`)),
|
|
208
|
+
);
|
|
209
|
+
server.tool(
|
|
210
|
+
'km_price_trend',
|
|
211
|
+
'What this business charges over time: the average and the typical (median) price of its events by month, quarter or year, with how many priced events sit behind every number and the same period a year earlier. ' +
|
|
212
|
+
'Split it by event type, by where the job came from (return client, referral, search and listing sites), by new versus returning clients, or by tag. With by: source it also says how many leads from each source were won. ' +
|
|
213
|
+
'Reach for it when someone asks whether their prices are going up, what a typical wedding or birthday sells for now, which clients pay the most, whether new clients are still coming in, or before any conversation about raising prices. ' +
|
|
214
|
+
'Read the caveats that come back and keep them: a row marked too_few is "too few to read" and carries no conclusion, lead with the typical price because the average moves with the mix of jobs, discounts that read "not recorded" are never 0%, and these numbers show that a price moved, never why, so never credit or blame marketing or brand work for a change. ' +
|
|
215
|
+
'For a long history use grain year or quarter; months split by event type over many years is a very long list. ' +
|
|
216
|
+
READ_ONLY,
|
|
217
|
+
{
|
|
218
|
+
grain: z
|
|
219
|
+
.enum(['month', 'quarter', 'year', 'all'])
|
|
220
|
+
.optional()
|
|
221
|
+
.describe('How to group the event dates. Default quarter. all gives one row per group for the whole window.'),
|
|
222
|
+
by: z
|
|
223
|
+
.enum(['none', 'event_type', 'source', 'client_type', 'tag'])
|
|
224
|
+
.optional()
|
|
225
|
+
.describe('How to split each period. Default none. source = where the job came from, client_type = new vs returning client.'),
|
|
226
|
+
from: z.string().optional().describe('Earliest EVENT date, YYYY-MM-DD. Leave out for the whole history.'),
|
|
227
|
+
to: z.string().optional().describe('Latest EVENT date, YYYY-MM-DD. Leave out for everything booked, future events included.'),
|
|
228
|
+
},
|
|
229
|
+
async ({ grain, by, from, to }) => {
|
|
230
|
+
const r = await call('GET', `/money/price-trend${qs({ grain: grain ?? 'quarter', by, from, to })}`);
|
|
231
|
+
if (routeMissing(r)) return text(PRICE_NOT_SUPPORTED, true);
|
|
232
|
+
return out(r);
|
|
233
|
+
},
|
|
234
|
+
);
|
|
235
|
+
}
|