@velaro/cli 1.4.107 → 1.4.108
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/bin/velaro.js +6 -0
- package/lib/commands/agent.js +11 -1
- package/lib/commands/avatar.js +3 -0
- package/lib/commands/billing-agents.js +51 -0
- package/lib/commands/magento.js +52 -0
- package/lib/commands/order-form.js +6 -1
- package/lib/commands/package-builder.js +35 -0
- package/lib/commands/price-increase.js +93 -0
- package/lib/commands/safety.js +78 -2
- package/lib/commands/teams-phone.js +63 -0
- package/package.json +1 -1
package/bin/velaro.js
CHANGED
|
@@ -97,6 +97,7 @@ import { billingContactsCommand } from '../lib/commands/billing-contacts.js';
|
|
|
97
97
|
import { billingSubscriptionCommand } from '../lib/commands/billing-subscription.js';
|
|
98
98
|
import { billingAutomationCommand } from '../lib/commands/billing-automation.js';
|
|
99
99
|
import { orderFormCommand } from '../lib/commands/order-form.js';
|
|
100
|
+
import { priceIncreaseCommand } from '../lib/commands/price-increase.js';
|
|
100
101
|
import { billingEmailPreferenceCommand } from '../lib/commands/billing-email-preference.js';
|
|
101
102
|
import { billingCommand } from '../lib/commands/billing.js';
|
|
102
103
|
import { couponGrantCommand } from '../lib/commands/coupon-grant.js';
|
|
@@ -104,6 +105,8 @@ import { signCommand } from '../lib/commands/sign.js';
|
|
|
104
105
|
import { widgetContainerCommand } from '../lib/commands/widget-container.js';
|
|
105
106
|
import { inlineWidgetConfigCommand } from '../lib/commands/inline-widget-config.js';
|
|
106
107
|
import { pricingCommand } from '../lib/commands/pricing.js';
|
|
108
|
+
import { billingAgentsCommand } from '../lib/commands/billing-agents.js';
|
|
109
|
+
import { packageBuilderCommand } from '../lib/commands/package-builder.js';
|
|
107
110
|
import { entitlementCommand } from '../lib/commands/entitlement.js';
|
|
108
111
|
import { subscriptionCommand } from '../lib/commands/subscription.js';
|
|
109
112
|
import { supportCommand } from '../lib/commands/support.js';
|
|
@@ -256,6 +259,7 @@ await yargs(hideBin(process.argv))
|
|
|
256
259
|
.command(billingSubscriptionCommand)
|
|
257
260
|
.command(billingAutomationCommand)
|
|
258
261
|
.command(orderFormCommand)
|
|
262
|
+
.command(priceIncreaseCommand)
|
|
259
263
|
.command(billingEmailPreferenceCommand)
|
|
260
264
|
.command(billingCommand)
|
|
261
265
|
.command(couponGrantCommand)
|
|
@@ -264,6 +268,8 @@ await yargs(hideBin(process.argv))
|
|
|
264
268
|
.command(inlineWidgetConfigCommand)
|
|
265
269
|
.command(pricingGroupCommand)
|
|
266
270
|
.command(entitlementGroup)
|
|
271
|
+
.command({ command: 'billing-agents', desc: 'Run and review the billing agents (staff)', builder: billingAgentsCommand, handler: () => {} })
|
|
272
|
+
.command({ command: 'package-builder', desc: 'Package graph, drafts, simulation, audit (staff)', builder: packageBuilderCommand, handler: () => {} })
|
|
267
273
|
.command({ command: 'subscription', desc: 'Apply/diff SubscriptionPackage packages on a site (superadmin only)', builder: subscriptionCommand, handler: () => {} })
|
|
268
274
|
.command(supportGroupCommand)
|
|
269
275
|
.command(routingCommand)
|
package/lib/commands/agent.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { get, post, messagingGet, messagingPost, messagingPut, getCredentials } from '../api.js';
|
|
2
2
|
import { runCommand } from '../run.js';
|
|
3
3
|
|
|
4
|
-
const VALID_STATUSES = ['Available', 'Away', 'Offline'];
|
|
4
|
+
const VALID_STATUSES = ['Available', 'Busy', 'Away', 'Offline'];
|
|
5
5
|
|
|
6
6
|
const MISSED_REASON_LABELS = {
|
|
7
7
|
NoAgentsOnline: 'No agents online',
|
|
@@ -120,6 +120,16 @@ export const agentCommand = {
|
|
|
120
120
|
console.log(`Agent ${argv.id} set to ${argv.status}.`);
|
|
121
121
|
}),
|
|
122
122
|
})
|
|
123
|
+
.command({
|
|
124
|
+
command: 'force-sign-out',
|
|
125
|
+
describe: 'Force an agent to sign in again: revokes all their sessions now and sets them Offline (administrator only, same site, audited)',
|
|
126
|
+
builder: (y) => y
|
|
127
|
+
.option('id', { type: 'number', demandOption: true, describe: 'Agent user ID' }),
|
|
128
|
+
handler: runCommand(async (argv) => {
|
|
129
|
+
await messagingPost(`/Users/${argv.id}/ForceSignOut`, {});
|
|
130
|
+
console.log(`Agent ${argv.id} was signed out of every session and set to Offline.`);
|
|
131
|
+
}),
|
|
132
|
+
})
|
|
123
133
|
.command({
|
|
124
134
|
command: 'missed-chats',
|
|
125
135
|
describe: 'Show recent missed chats with routing snapshots (why each chat was missed)',
|
package/lib/commands/avatar.js
CHANGED
|
@@ -35,6 +35,7 @@ export const avatarCommand = {
|
|
|
35
35
|
console.log(` Enabled: ${data.enabled ? 'yes' : 'no'}`);
|
|
36
36
|
console.log(` "Call me": ${data.showCallMeOption ? 'yes' : 'no'}`);
|
|
37
37
|
console.log(` Callback window: ${data.callbackConfirmWindowMinutes} min`);
|
|
38
|
+
console.log(` Show transcript: ${data.showTranscript ? 'yes' : 'no'}`);
|
|
38
39
|
console.log(` Display name: ${data.displayName || '(AI)'}`);
|
|
39
40
|
console.log(` Badge shown: ${data.showBadge ? 'yes' : 'no'}`);
|
|
40
41
|
console.log(` Size: ${data.sizePx}px`);
|
|
@@ -78,6 +79,7 @@ export const avatarCommand = {
|
|
|
78
79
|
.option('avatar-id',{ type: 'string', describe: 'Provider-specific avatar/face ID. Omit for provider default.' })
|
|
79
80
|
.option('voice', { type: 'string', describe: 'ElevenLabs voice ID override' })
|
|
80
81
|
.option('keep-on-agent-join', { type: 'boolean', describe: 'Keep the avatar running after a human agent joins. Omit on a re-run to leave it untouched.' })
|
|
82
|
+
.option('show-transcript', { type: 'boolean', describe: 'Show an on-screen avatar transcript (default off). Omit on a re-run to leave it untouched.' })
|
|
81
83
|
.option('call-me', { type: 'boolean', describe: 'Show the "Call me instead" option. Omit on a re-run to leave it untouched.' })
|
|
82
84
|
.option('display-name', { type: 'string', describe: 'Visitor-facing avatar name on the badge (e.g. Moshky). Empty string resets to "AI".' })
|
|
83
85
|
.option('show-badge', { type: 'boolean', describe: 'Show the name badge over the avatar video.' })
|
|
@@ -97,6 +99,7 @@ export const avatarCommand = {
|
|
|
97
99
|
if (argv.voice !== undefined) body.voiceId = argv.voice;
|
|
98
100
|
if (argv['keep-on-agent-join'] !== undefined) body.enabledOnAgentTransfer = argv['keep-on-agent-join'];
|
|
99
101
|
if (argv['call-me'] !== undefined) body.showCallMeOption = argv['call-me'];
|
|
102
|
+
if (argv['show-transcript'] !== undefined) body.showTranscript = argv['show-transcript'];
|
|
100
103
|
if (argv['display-name'] !== undefined) body.displayName = argv['display-name'];
|
|
101
104
|
if (argv['show-badge'] !== undefined) body.showBadge = argv['show-badge'];
|
|
102
105
|
if (argv.size !== undefined) body.sizePx = argv.size;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// velaro billing-agents: run and review the billing agents (staff only). Contract: docs/architecture/billing-agents-and-package-builder.md
|
|
2
|
+
import { request } from '../api.js';
|
|
3
|
+
|
|
4
|
+
const show = (o) => console.log(JSON.stringify(o, null, 2));
|
|
5
|
+
|
|
6
|
+
async function list() {
|
|
7
|
+
const agents = await request('GET', '/BillingAgents');
|
|
8
|
+
for (const a of agents) console.log(`${a.key.padEnd(12)} ${a.name.padEnd(18)} open findings: ${a.openFindings} last run: ${a.lastRun ? `${a.lastRun.mode}/${a.lastRun.status} ${a.lastRun.startedAt}` : 'never'}`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function run(args) {
|
|
12
|
+
const mode = args.live ? 'live' : 'test';
|
|
13
|
+
const body = { mode };
|
|
14
|
+
if (args.site) body.siteId = args.site;
|
|
15
|
+
if (args.days) body.params = { days: args.days };
|
|
16
|
+
const r = await request('POST', `/BillingAgents/${args.agent}/run`, body);
|
|
17
|
+
console.log(`Run ${r.run.id} [${r.run.mode}] ${r.run.status}: ${r.findings.length} findings${mode === 'test' ? ' (dry run, no side effects)' : ''}`);
|
|
18
|
+
for (const f of r.findings) console.log(` #${f.id} ${f.severity.padEnd(5)} ${f.ruleKey.padEnd(26)} ${f.title}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function runs(args) {
|
|
22
|
+
const q = new URLSearchParams();
|
|
23
|
+
if (args.agent) q.set('agentKey', args.agent);
|
|
24
|
+
if (args.mode) q.set('mode', args.mode);
|
|
25
|
+
show(await request('GET', `/BillingAgents/runs?${q}`));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function findings(args) {
|
|
29
|
+
const q = new URLSearchParams();
|
|
30
|
+
for (const k of ['status', 'severity']) if (args[k]) q.set(k, args[k]);
|
|
31
|
+
if (args.agent) q.set('agentKey', args.agent);
|
|
32
|
+
show(await request('GET', `/BillingAgents/findings?${q}`));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const step = (verb) => async (args) => show(await request('POST', `/BillingAgents/findings/${args.id}/${verb}`, {}));
|
|
36
|
+
|
|
37
|
+
export function billingAgentsCommand(yargs) {
|
|
38
|
+
return yargs
|
|
39
|
+
.command('list', 'List agents with last run and open findings', () => {}, () => list())
|
|
40
|
+
.command('run <agent>', 'Run an agent (default TEST mode = dry run)', y => y
|
|
41
|
+
.positional('agent', { type: 'string', desc: 'close|collections|analyst|pricing|orders|utilization|syncfailure' })
|
|
42
|
+
.option('live', { type: 'boolean', desc: 'Live mode (findings + approval queue; still no side effects until apply)' })
|
|
43
|
+
.option('site', { type: 'number' }).option('days', { type: 'number' }), a => run(a))
|
|
44
|
+
.command('runs', 'List runs', y => y.option('agent', { type: 'string' }).option('mode', { type: 'string' }), a => runs(a))
|
|
45
|
+
.command('findings', 'List findings', y => y.option('status', { type: 'string' }).option('severity', { type: 'string' }).option('agent', { type: 'string' }), a => findings(a))
|
|
46
|
+
.command('approve <id>', 'Approve a finding (two-person rule for financial actions)', y => y.positional('id', { type: 'number' }), step('approve'))
|
|
47
|
+
.command('reject <id>', 'Reject a finding', y => y.positional('id', { type: 'number' }), step('reject'))
|
|
48
|
+
.command('apply <id>', 'Apply an approved finding (draft-safe actions only)', y => y.positional('id', { type: 'number' }), step('apply'))
|
|
49
|
+
.command('monitor', 'Agent health: last runs, 7d failure rate, stale agents', () => {}, async () => show(await request('GET', '/BillingAgents/monitor')))
|
|
50
|
+
.demandCommand(1, 'Specify a subcommand: list | run | runs | findings | approve | reject | apply | monitor');
|
|
51
|
+
}
|
package/lib/commands/magento.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* velaro magento products Search the product catalog
|
|
9
9
|
* velaro magento logs View integration activity logs
|
|
10
10
|
* velaro magento diagnose Live connection diagnostic (token, /rest/V1, scopes)
|
|
11
|
+
* velaro magento sidebar-fields get|set Agent sidebar fields (site from your login)
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
import { request, messagingRequest } from '../api.js';
|
|
@@ -189,6 +190,48 @@ async function diagnoseHandler(argv) {
|
|
|
189
190
|
}
|
|
190
191
|
}
|
|
191
192
|
|
|
193
|
+
// ── sidebar-fields ────────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
async function sidebarFieldsHandler(argv) {
|
|
196
|
+
const action = argv.action;
|
|
197
|
+
try {
|
|
198
|
+
if (action === 'get') {
|
|
199
|
+
const r = await messagingRequest('GET', '/Magento/data/sidebar-fields');
|
|
200
|
+
console.log(JSON.stringify(r, null, 2));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (action !== 'set') {
|
|
204
|
+
console.error(`${RED}✗ action must be get or set${RST}`);
|
|
205
|
+
process.exitCode = 1;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
let fields;
|
|
209
|
+
if (argv.clear) {
|
|
210
|
+
fields = [];
|
|
211
|
+
} else if (argv.fields !== undefined) {
|
|
212
|
+
try { fields = JSON.parse(argv.fields); } catch (e) {
|
|
213
|
+
console.error(`${RED}✗ --fields must be valid JSON: ${e.message}${RST}`);
|
|
214
|
+
process.exitCode = 1;
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (!Array.isArray(fields)) {
|
|
218
|
+
console.error(`${RED}✗ --fields must be a JSON array of {fieldName, entity, label}${RST}`);
|
|
219
|
+
process.exitCode = 1;
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Partial body: omitting --fields leaves the saved list untouched.
|
|
224
|
+
const body = fields === undefined ? {} : { fields };
|
|
225
|
+
const r = await messagingRequest('PUT', '/Magento/data/sidebar-fields', body);
|
|
226
|
+
console.log(JSON.stringify(r, null, 2));
|
|
227
|
+
} catch (e) {
|
|
228
|
+
console.error(`
|
|
229
|
+
${RED}✗ sidebar-fields ${action} failed: ${e.message}${RST}
|
|
230
|
+
`);
|
|
231
|
+
process.exitCode = 1;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
192
235
|
// ── command export ────────────────────────────────────────────────────────────
|
|
193
236
|
|
|
194
237
|
export const magentoCommand = {
|
|
@@ -250,6 +293,15 @@ export const magentoCommand = {
|
|
|
250
293
|
.option('site-id', { describe: 'Site ID to diagnose', type: 'number' }),
|
|
251
294
|
handler: diagnoseHandler,
|
|
252
295
|
})
|
|
296
|
+
.command({
|
|
297
|
+
command: 'sidebar-fields <action>',
|
|
298
|
+
describe: 'Get or set the Magento agent sidebar fields (action: get|set)',
|
|
299
|
+
builder: yy => yy
|
|
300
|
+
.positional('action', { choices: ['get', 'set'], type: 'string' })
|
|
301
|
+
.option('fields', { describe: 'JSON array of {fieldName, entity, label} (set only)', type: 'string' })
|
|
302
|
+
.option('clear', { describe: 'Clear the saved list (set only)', type: 'boolean', default: false }),
|
|
303
|
+
handler: sidebarFieldsHandler,
|
|
304
|
+
})
|
|
253
305
|
.demandCommand(1, 'Specify a subcommand. Try: velaro magento --help'),
|
|
254
306
|
handler: () => {},
|
|
255
307
|
};
|
|
@@ -21,6 +21,7 @@ function parseLine(spec) {
|
|
|
21
21
|
|
|
22
22
|
function printForm(f) {
|
|
23
23
|
console.log(`Order form ${f.id}: ${f.customerName} [${f.status}]`);
|
|
24
|
+
if (f.annualIncreasePercent != null) console.log(` annual increase ${f.annualIncreasePercent}% on ${f.increaseBasis ?? 'site default'} price`);
|
|
24
25
|
console.log(` payment=${f.paymentMode} invoiceDetail=${f.invoiceDetailMode} applyOnSign=${f.applyOnSign} contractId=${f.contractId ?? '-'}`);
|
|
25
26
|
for (const l of f.lines ?? []) {
|
|
26
27
|
console.log(` - ${l.catalogSlug} x${l.quantity} @ ${money(l.unitPriceCents)} (list ${money(l.listPriceCents)}) setup ${money(l.setupFeeCents)} ${l.termMonths}mo ${l.cadence}`);
|
|
@@ -30,7 +31,7 @@ function printForm(f) {
|
|
|
30
31
|
async function createForm(args) {
|
|
31
32
|
const lines = [].concat(args.line ?? []).map(parseLine);
|
|
32
33
|
if (!args.customer || lines.length === 0) {
|
|
33
|
-
console.error('Usage: velaro order-form create --customer "Acme" --line slug:qty[:unitCents[:setupCents[:termMonths[:cadence]]]] [--line ...] [--site-id N] [--target v20] [--target-account-id X] [--payment-mode manual|card_checkout|invoice_net_terms] [--invoice-detail LineItemized|TermsReference] [--apply-on-sign]');
|
|
34
|
+
console.error('Usage: velaro order-form create --customer "Acme" --line slug:qty[:unitCents[:setupCents[:termMonths[:cadence]]]] [--line ...] [--site-id N] [--target v20] [--target-account-id X] [--payment-mode manual|card_checkout|invoice_net_terms] [--invoice-detail LineItemized|TermsReference] [--apply-on-sign] [--increase-percent 8] [--increase-basis list|net]');
|
|
34
35
|
process.exit(1);
|
|
35
36
|
}
|
|
36
37
|
const body = {
|
|
@@ -41,6 +42,8 @@ async function createForm(args) {
|
|
|
41
42
|
paymentMode: args.paymentMode,
|
|
42
43
|
invoiceDetailMode: args.invoiceDetail,
|
|
43
44
|
applyOnSign: !!args.applyOnSign,
|
|
45
|
+
annualIncreasePercent: args.increasePercent,
|
|
46
|
+
increaseBasis: args.increaseBasis,
|
|
44
47
|
lines,
|
|
45
48
|
};
|
|
46
49
|
const f = await request('POST', '/api/OrderForm', body);
|
|
@@ -91,6 +94,8 @@ export const orderFormCommand = {
|
|
|
91
94
|
'payment-mode': { type: 'string', choices: ['manual', 'card_checkout', 'invoice_net_terms'] },
|
|
92
95
|
'invoice-detail': { type: 'string', choices: ['LineItemized', 'TermsReference'] },
|
|
93
96
|
'apply-on-sign': { type: 'boolean', default: false },
|
|
97
|
+
'increase-percent': { type: 'number', describe: 'annual price increase percent stated in the contract' },
|
|
98
|
+
'increase-basis': { type: 'string', choices: ['list', 'net'], describe: 'calculate the increase on the undiscounted (list) or discounted (net) price; omit for the site default' },
|
|
94
99
|
}, (args) => createForm(args))
|
|
95
100
|
.command('get <id>', 'Show an order form', {}, (args) => getForm(args.id))
|
|
96
101
|
.command('list', 'List order forms', { status: { type: 'string' } }, (args) => listForms(args))
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// velaro package-builder: package graph, drafts, simulation, audit (staff only; `my` is customer-safe).
|
|
2
|
+
import { request } from '../api.js';
|
|
3
|
+
|
|
4
|
+
const show = (o) => console.log(JSON.stringify(o, null, 2));
|
|
5
|
+
const get = (path) => async () => show(await request('GET', path));
|
|
6
|
+
const enc = encodeURIComponent;
|
|
7
|
+
|
|
8
|
+
export function packageBuilderCommand(yargs) {
|
|
9
|
+
return yargs
|
|
10
|
+
.command('graph', 'Features, packages, add-ons and standalone flags', () => {}, get('/PackageBuilder/graph'))
|
|
11
|
+
.command('site <siteId>', 'Per-flag provenance for a site', y => y.positional('siteId', { type: 'number' }), async a => show(await request('GET', `/PackageBuilder/site/${a.siteId}`)))
|
|
12
|
+
.command('versions <planKey>', 'Version history of a plan', y => y.positional('planKey', { type: 'string' }), async a => show(await request('GET', `/PackageBuilder/versions/${enc(a.planKey)}`)))
|
|
13
|
+
.command('diff', 'Diff two plan versions', y => y.option('plan', { type: 'string', demandOption: true }).option('from', { type: 'number', demandOption: true }).option('to', { type: 'number', demandOption: true }),
|
|
14
|
+
async a => show(await request('GET', `/PackageBuilder/diff?planKey=${enc(a.plan)}&from=${a.from}&to=${a.to}`)))
|
|
15
|
+
.command('drafts', 'List drafts', y => y.option('status', { type: 'string' }), async a => show(await request('GET', `/PackageBuilder/drafts${a.status ? `?status=${enc(a.status)}` : ''}`)))
|
|
16
|
+
.command('draft-create', 'Create a draft (from scratch or --clone-of plan.key)', y => y
|
|
17
|
+
.option('name', { type: 'string' }).option('clone-of', { type: 'string' }).option('features', { type: 'string', desc: 'comma-separated feature keys' }),
|
|
18
|
+
async a => show(await request('POST', '/PackageBuilder/drafts', { name: a.name, cloneOf: a['clone-of'], definition: a.features ? { features: a.features.split(',') } : undefined })))
|
|
19
|
+
.command('draft-update <id>', 'Replace a draft definition', y => y.positional('id', { type: 'number' }).option('features', { type: 'string', demandOption: true }).option('price-cents', { type: 'number' }).option('name', { type: 'string' }),
|
|
20
|
+
async a => show(await request('PUT', `/PackageBuilder/drafts/${a.id}`, { name: a.name, definition: { features: a.features.split(','), priceCents: a['price-cents'] } })))
|
|
21
|
+
.command('submit <id>', 'Submit draft for approval', y => y.positional('id', { type: 'number' }), async a => show(await request('POST', `/PackageBuilder/drafts/${a.id}/submit`, {})))
|
|
22
|
+
.command('approve <id>', 'Approve draft (approver must differ from creator)', y => y.positional('id', { type: 'number' }), async a => show(await request('POST', `/PackageBuilder/drafts/${a.id}/approve`, {})))
|
|
23
|
+
.command('publish <id>', 'Publish approved draft as new PlanEntitlement version rows', y => y.positional('id', { type: 'number' }), async a => show(await request('POST', `/PackageBuilder/drafts/${a.id}/publish`, {})))
|
|
24
|
+
.command('simulate', 'What-if across a cohort (pure read)', y => y.option('draft', { type: 'number' }).option('plan', { type: 'string' }).option('cohort', { type: 'string' }),
|
|
25
|
+
async a => show(await request('POST', '/PackageBuilder/simulate', { draftId: a.draft, basePlanKey: a.plan, cohort: a.cohort })))
|
|
26
|
+
.command('audit', 'Entitlement/package change timeline', y => y.option('site', { type: 'number' }).option('plan', { type: 'string' }),
|
|
27
|
+
async a => show(await request('GET', `/PackageBuilder/audit?${new URLSearchParams({ ...(a.site ? { siteId: a.site } : {}), ...(a.plan ? { planKey: a.plan } : {}) })}`)))
|
|
28
|
+
.command('health', 'Orphan flags, overlap, drift, cart parity', () => {}, get('/PackageBuilder/health'))
|
|
29
|
+
.command('fit', 'Cheapest package covering the given features', y => y.option('features', { type: 'string', demandOption: true }),
|
|
30
|
+
async a => show(await request('POST', '/PackageBuilder/fit', { featureKeys: a.features.split(',') })))
|
|
31
|
+
.command('proration', 'Upgrade/downgrade proration preview', y => y.option('from', { type: 'string', demandOption: true }).option('to', { type: 'string', demandOption: true }).option('days-remaining', { type: 'number', demandOption: true }).option('days-in-period', { type: 'number', default: 30 }),
|
|
32
|
+
async a => show(await request('GET', `/PackageBuilder/proration?fromPlan=${enc(a.from)}&toPlan=${enc(a.to)}&daysRemaining=${a['days-remaining']}&daysInPeriod=${a['days-in-period']}`)))
|
|
33
|
+
.command('my', 'Your own package and what it includes (customer-safe)', () => {}, get('/PackageBuilder/my'))
|
|
34
|
+
.demandCommand(1, 'Specify a subcommand');
|
|
35
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// velaro price-increase -- staff-only annual price increase basis (PriceIncreaseController).
|
|
2
|
+
// See docs/architecture/price-increase-basis.md.
|
|
3
|
+
// basis "list": increase = % of the undiscounted price, added to what the customer pays (discount stays the same dollars)
|
|
4
|
+
// basis "net": increase = % of what the customer pays (discount stays the same percent)
|
|
5
|
+
// Nothing here appears on an invoice; every applied increase is written to an internal audit log.
|
|
6
|
+
import { request } from '../api.js';
|
|
7
|
+
|
|
8
|
+
const BASE = '/api/PriceIncrease';
|
|
9
|
+
|
|
10
|
+
function money(cents) {
|
|
11
|
+
if (cents === null || cents === undefined) return '-';
|
|
12
|
+
return `$${(Number(cents) / 100).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function getSettings() {
|
|
16
|
+
const s = await request('GET', `${BASE}/settings`);
|
|
17
|
+
console.log(`Default increase basis: ${s.defaultBasis}${s.isExplicitlySet ? '' : ' (never set, using the built-in default)'}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function setDefault(args) {
|
|
21
|
+
if (!['list', 'net', 'clear'].includes(args.basis)) {
|
|
22
|
+
console.error('Usage: velaro price-increase set-default <list|net|clear>');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const r = await request('PUT', `${BASE}/settings`, { defaultBasis: args.basis === 'clear' ? null : args.basis });
|
|
26
|
+
console.log(`Default increase basis is now: ${r.defaultBasis}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function showSubscription(id) {
|
|
30
|
+
const t = await request('GET', `${BASE}/subscriptions/${id}`);
|
|
31
|
+
console.log(`Subscription ${t.subscriptionId}`);
|
|
32
|
+
console.log(` net per period: ${money(t.recurringAmountCents)} list per period: ${money(t.recurringListAmountCents)}`);
|
|
33
|
+
console.log(` basis: ${t.effectiveBasis} (from ${t.effectiveBasisSource}${t.basis ? '' : ', inherited'}) annual increase: ${t.annualIncreasePercent ?? '-'}%`);
|
|
34
|
+
console.log(` next increase: ${t.nextPriceIncreaseDate?.slice(0, 10) ?? '-'} applied so far: ${t.priceIncreasesApplied}${t.needsReview ? ' NEEDS REVIEW (list price missing)' : ''}`);
|
|
35
|
+
for (const l of t.log ?? []) {
|
|
36
|
+
console.log(` year ${l.contractYearIndex} ${l.anniversaryDate?.slice(0, 10)} ${l.applied ? 'APPLIED' : 'SKIPPED'}: ${l.formula}${l.invoiceId ? ` (invoice ${l.invoiceId})` : ''}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function setSubscription(id, args) {
|
|
41
|
+
const body = {};
|
|
42
|
+
if (args.basis !== undefined) body.basis = args.basis === 'inherit' ? null : args.basis;
|
|
43
|
+
if (args.percent !== undefined) body.annualIncreasePercent = args.percent === 'none' ? null : Number(args.percent);
|
|
44
|
+
if (args.listCents !== undefined) body.recurringListAmountCents = Number(args.listCents);
|
|
45
|
+
if (args.next !== undefined) body.nextPriceIncreaseDate = args.next === 'none' ? null : args.next;
|
|
46
|
+
if (Object.keys(body).length === 0) {
|
|
47
|
+
console.error('Usage: velaro price-increase set <subscriptionId> [--basis list|net|inherit] [--percent 8|none] [--list-cents 1000000] [--next 2027-01-01|none]');
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
const r = await request('PUT', `${BASE}/subscriptions/${id}`, body);
|
|
51
|
+
console.log(r.changes?.length ? `Updated: ${r.changes.join('; ')}` : 'No change.');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function linkOrderForm(id, formId) {
|
|
55
|
+
const r = await request('POST', `${BASE}/subscriptions/${id}/link-order-form/${formId}`, {});
|
|
56
|
+
console.log(r.summary ?? 'Linked.');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function preview(args) {
|
|
60
|
+
const r = await request('POST', `${BASE}/preview`, {
|
|
61
|
+
listCents: Number(args.listCents), netCents: Number(args.netCents), percent: Number(args.percent), years: args.years,
|
|
62
|
+
});
|
|
63
|
+
console.log('Year LIST basis (net / discount) NET basis (net / discount)');
|
|
64
|
+
r.list.forEach((row, i) => {
|
|
65
|
+
const n = r.net[i];
|
|
66
|
+
console.log(`${String(row.year).padEnd(5)} ${(money(row.netCents) + ' / ' + money(row.discountCents)).padEnd(34)} ${money(n.netCents)} / ${money(n.discountCents)}`);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const priceIncreaseCommand = {
|
|
71
|
+
command: 'price-increase <subcommand>',
|
|
72
|
+
describe: 'Annual price increase basis: list vs discounted price (staff only)',
|
|
73
|
+
builder: (yargs) =>
|
|
74
|
+
yargs
|
|
75
|
+
.command('settings', 'Show the site-wide default basis', {}, () => getSettings())
|
|
76
|
+
.command('set-default <basis>', 'Set the site-wide default basis: list | net | clear', {}, (args) => setDefault(args))
|
|
77
|
+
.command('show <id>', 'Show a subscription\'s increase terms and its audit log', {}, (args) => showSubscription(args.id))
|
|
78
|
+
.command('set <id>', 'Update a subscription\'s increase terms (only the flags you pass change)', {
|
|
79
|
+
basis: { type: 'string', choices: ['list', 'net', 'inherit'] },
|
|
80
|
+
percent: { type: 'string', describe: 'annual percent, or "none" to clear' },
|
|
81
|
+
'list-cents': { type: 'number', describe: 'undiscounted price per billing period, in cents' },
|
|
82
|
+
next: { type: 'string', describe: 'next anniversary date (YYYY-MM-DD) or "none"' },
|
|
83
|
+
}, (args) => setSubscription(args.id, args))
|
|
84
|
+
.command('link-order-form <id> <formId>', 'Copy the increase terms of a SIGNED order form (basis, percent, per-line exceptions) onto a subscription so billing follows the contract', {}, (args) => linkOrderForm(args.id, args.formId))
|
|
85
|
+
.command('preview', 'Compare both bases side by side (no changes made)', {
|
|
86
|
+
'list-cents': { type: 'number', demandOption: true },
|
|
87
|
+
'net-cents': { type: 'number', demandOption: true },
|
|
88
|
+
percent: { type: 'number', demandOption: true },
|
|
89
|
+
years: { type: 'number' },
|
|
90
|
+
}, (args) => preview(args))
|
|
91
|
+
.demandCommand(1),
|
|
92
|
+
handler: () => {},
|
|
93
|
+
};
|
package/lib/commands/safety.js
CHANGED
|
@@ -40,9 +40,56 @@ function printSettings(s) {
|
|
|
40
40
|
console.log(`Count alert: ${s.threshold?.count ?? 0} events in ${s.threshold?.windowMinutes ?? 60} minutes`);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
|
|
44
|
+
// ── Safety policy (Compliance > Safety Controls) ─────────────────────────────────────────────
|
|
45
|
+
// Wraps SafetyPolicyController. Customer routes take the site from login; Velaro staff pass --site to use the staff routes.
|
|
46
|
+
// Turning a control OFF needs the recorded liability acknowledgement: the CLI prints the exact text and only sends it when you pass --accept.
|
|
47
|
+
const policyPath = (a) => (a.site ? `/SafetyPolicy/staff/${Number(a.site)}` : '/SafetyPolicy');
|
|
48
|
+
const AUDIENCES = ['ai_bot', 'human_agent'];
|
|
49
|
+
|
|
50
|
+
function printPolicy(v) {
|
|
51
|
+
console.log('Safety controls (on by default):');
|
|
52
|
+
for (const c of v.controls ?? []) {
|
|
53
|
+
console.log(` ${c.control}: AI bot ${c.aiBot ? 'on' : 'OFF'}, human agent ${c.humanAgent ? 'on' : 'OFF'}`);
|
|
54
|
+
}
|
|
55
|
+
console.log(`Always on (floor): ${(v.floor ?? []).join(', ')}`);
|
|
56
|
+
if (v.anyOff) console.log(`\nWARNING: ${v.warning}`);
|
|
57
|
+
const feats = Object.keys(v.overrides?.features ?? {});
|
|
58
|
+
if (feats.length) console.log(`Per-feature overrides: ${feats.join(', ')}`);
|
|
59
|
+
console.log(`Acknowledgement version: ${v.acknowledgement?.version}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function applyChange(a, enabled) {
|
|
63
|
+
const audiences = a.audience === 'both' ? AUDIENCES : [a.audience];
|
|
64
|
+
let last;
|
|
65
|
+
if (enabled === false) {
|
|
66
|
+
if (!a.accept) {
|
|
67
|
+
const cur = await messagingGet(policyPath(a));
|
|
68
|
+
console.error(`Turning off a safety control needs the acknowledgement. Read it, then re-run with --accept:\n\n${cur.acknowledgement?.text}\n`);
|
|
69
|
+
throw new Error('Acknowledgement not accepted (pass --accept after reading the text above).');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const ack = enabled === false ? (await messagingGet(policyPath(a))).acknowledgement : null;
|
|
73
|
+
for (const audience of audiences) {
|
|
74
|
+
last = await messagingPut(policyPath(a), {
|
|
75
|
+
control: a.control,
|
|
76
|
+
audience,
|
|
77
|
+
featureKey: a.feature ?? null,
|
|
78
|
+
enabled,
|
|
79
|
+
...(enabled === false ? { acknowledged: true, ackTextVersion: ack.version } : {}),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return last;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const siteOpt = { type: 'number', describe: 'Velaro staff only: target site ID (uses the staff route)' };
|
|
86
|
+
const controlOpt = { type: 'string', demandOption: true, describe: 'emergency_self_harm | abuse_ladder | threat_detection | input_jailbreak | output_filter' };
|
|
87
|
+
const audienceOpt = { type: 'string', choices: [...AUDIENCES, 'both'], default: 'both' };
|
|
88
|
+
const featureOpt = { type: 'string', describe: 'Per-feature/channel scope (chat, voice, sms, whatsapp, email). Omit for the whole account.' };
|
|
89
|
+
|
|
43
90
|
export const safetyCommand = {
|
|
44
91
|
command: 'safety',
|
|
45
|
-
describe: 'Safety events and
|
|
92
|
+
describe: 'Safety events, alerts and safety controls: list events, acknowledge, alert settings, policy (on/off controls), test alert, ask Velaro for help',
|
|
46
93
|
builder: (yargs) => yargs
|
|
47
94
|
.command('events', 'List safety events', (y) => y
|
|
48
95
|
.option('from', { type: 'string', describe: 'ISO start' })
|
|
@@ -106,6 +153,35 @@ export const safetyCommand = {
|
|
|
106
153
|
await messagingPost('/SafetyEvents/need-help', { message: a.message });
|
|
107
154
|
console.log('Sent. The Velaro team has been alerted.');
|
|
108
155
|
}))
|
|
109
|
-
.
|
|
156
|
+
.command('policy <action>', 'Safety controls on/off policy (get | set)', (y) => y
|
|
157
|
+
.positional('action', { choices: ['get', 'set'] })
|
|
158
|
+
.option('site', siteOpt)
|
|
159
|
+
.option('control', { ...controlOpt, demandOption: false })
|
|
160
|
+
.option('audience', audienceOpt)
|
|
161
|
+
.option('feature', featureOpt)
|
|
162
|
+
.option('enabled', { type: 'boolean', describe: 'true = on, false = off (off needs --accept)' })
|
|
163
|
+
.option('reset', { type: 'boolean', describe: 'Clear the override and use the default (on)' })
|
|
164
|
+
.option('accept', { type: 'boolean', describe: 'Accept the liability acknowledgement (required to turn a control off)' })
|
|
165
|
+
.option('json', { type: 'boolean' }),
|
|
166
|
+
runCommand(async (a) => {
|
|
167
|
+
if (a.action === 'get') return out(a, await messagingGet(policyPath(a)), printPolicy);
|
|
168
|
+
if (!a.control) throw new Error('--control is required for set.');
|
|
169
|
+
if (a.reset && a.enabled !== undefined) throw new Error('Use either --enabled or --reset, not both.');
|
|
170
|
+
if (!a.reset && a.enabled === undefined) throw new Error('Pass --enabled true|false or --reset.');
|
|
171
|
+
const v = await applyChange(a, a.reset ? null : a.enabled);
|
|
172
|
+
out(a, v, (x) => { console.log('Saved.\n'); printPolicy(x); });
|
|
173
|
+
}))
|
|
174
|
+
.command('acknowledge-off', 'Turn a safety control OFF after accepting the liability acknowledgement', (y) => y
|
|
175
|
+
.option('site', siteOpt)
|
|
176
|
+
.option('control', controlOpt)
|
|
177
|
+
.option('audience', audienceOpt)
|
|
178
|
+
.option('feature', featureOpt)
|
|
179
|
+
.option('accept', { type: 'boolean', describe: 'I have read the acknowledgement text and accept it' })
|
|
180
|
+
.option('json', { type: 'boolean' }),
|
|
181
|
+
runCommand(async (a) => {
|
|
182
|
+
const v = await applyChange(a, false);
|
|
183
|
+
out(a, v, (x) => { console.log(`${a.control} turned off. The acceptance was recorded.\n`); printPolicy(x); });
|
|
184
|
+
}))
|
|
185
|
+
.demandCommand(1, 'Specify a subcommand: events, summary, ack, settings, policy, acknowledge-off, test, need-help'),
|
|
110
186
|
handler: () => {},
|
|
111
187
|
};
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* velaro teams-phone set --tenant-id ... Create or update the configuration
|
|
6
6
|
* velaro teams-phone remove Delete the configuration
|
|
7
7
|
* velaro teams-phone test Run the inbound-calling readiness checks
|
|
8
|
+
* velaro teams-phone agents list List agents and their Teams user object IDs
|
|
9
|
+
* velaro teams-phone agents set <agentId> <teamsUserId> Map an agent to an Entra user Object ID
|
|
10
|
+
* velaro teams-phone agents delete <agentId> Clear an agent's mapping
|
|
8
11
|
*
|
|
9
12
|
* Backend contract: server/Velaro.Messaging/Controllers/Voice/TeamsPhoneConfigController.cs
|
|
10
13
|
* (velaro-messaging). Site is resolved server-side from the auth context — never
|
|
@@ -98,6 +101,39 @@ async function testHandler() {
|
|
|
98
101
|
if (!result?.success) process.exitCode = 1;
|
|
99
102
|
}
|
|
100
103
|
|
|
104
|
+
// -- agents (agent -> Entra user object id) ---------------------------------------
|
|
105
|
+
|
|
106
|
+
async function agentsListHandler() {
|
|
107
|
+
const agents = await messagingGet('/TeamsPhone/Agents');
|
|
108
|
+
if (!Array.isArray(agents) || agents.length === 0) {
|
|
109
|
+
console.log(`
|
|
110
|
+
${DIM}No agents found.${RST}
|
|
111
|
+
`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
console.log(`
|
|
115
|
+
${BOLD}Teams Phone agent mapping${RST}`);
|
|
116
|
+
for (const a of agents) {
|
|
117
|
+
const name = `${a.firstName ?? ''} ${a.lastName ?? ''}`.trim();
|
|
118
|
+
console.log(` ${String(a.id).padEnd(8)} ${name.padEnd(28)} ${String(a.email ?? '').padEnd(34)} ${a.teamsUserId ? GRN + a.teamsUserId + RST : DIM + '(unmapped)' + RST}`);
|
|
119
|
+
}
|
|
120
|
+
console.log('');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function agentsSetHandler(argv) {
|
|
124
|
+
const r = await messagingPut(`/TeamsPhone/Agents/${encodeURIComponent(argv.agentId)}`, { teamsUserId: argv.teamsUserId });
|
|
125
|
+
console.log(`
|
|
126
|
+
${GRN}✓${RST} Agent ${argv.agentId} mapped to Teams user ${r?.teamsUserId ?? argv.teamsUserId}.
|
|
127
|
+
`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function agentsDeleteHandler(argv) {
|
|
131
|
+
await messagingDel(`/TeamsPhone/Agents/${encodeURIComponent(argv.agentId)}`);
|
|
132
|
+
console.log(`
|
|
133
|
+
${GRN}✓${RST} Agent ${argv.agentId} mapping cleared.
|
|
134
|
+
`);
|
|
135
|
+
}
|
|
136
|
+
|
|
101
137
|
// -- command export ------------------------------------------------------------------
|
|
102
138
|
|
|
103
139
|
export const teamsPhoneCommand = {
|
|
@@ -137,6 +173,33 @@ export const teamsPhoneCommand = {
|
|
|
137
173
|
builder: yy => yy,
|
|
138
174
|
handler: testHandler,
|
|
139
175
|
})
|
|
176
|
+
.command({
|
|
177
|
+
command: 'agents <action>',
|
|
178
|
+
describe: 'Agent mapping: which Entra user Object ID is which Velaro agent (list | set | delete)',
|
|
179
|
+
builder: yy => yy
|
|
180
|
+
.command({
|
|
181
|
+
command: 'list',
|
|
182
|
+
describe: 'List agents with their Teams user Object ID',
|
|
183
|
+
builder: z => z,
|
|
184
|
+
handler: agentsListHandler,
|
|
185
|
+
})
|
|
186
|
+
.command({
|
|
187
|
+
command: 'set <agentId> <teamsUserId>',
|
|
188
|
+
describe: 'Map a Velaro agent (workspace user id) to a Microsoft Entra user Object ID (GUID)',
|
|
189
|
+
builder: z => z
|
|
190
|
+
.positional('agentId', { type: 'number', describe: 'Velaro agent (workspace user) id' })
|
|
191
|
+
.positional('teamsUserId', { type: 'string', describe: 'Entra user Object ID (GUID); "" clears' }),
|
|
192
|
+
handler: agentsSetHandler,
|
|
193
|
+
})
|
|
194
|
+
.command({
|
|
195
|
+
command: 'delete <agentId>',
|
|
196
|
+
describe: 'Clear the Teams user mapping for a Velaro agent',
|
|
197
|
+
builder: z => z.positional('agentId', { type: 'number', describe: 'Velaro agent (workspace user) id' }),
|
|
198
|
+
handler: agentsDeleteHandler,
|
|
199
|
+
})
|
|
200
|
+
.demandCommand(1, 'Specify list, set or delete.'),
|
|
201
|
+
handler: () => {},
|
|
202
|
+
})
|
|
140
203
|
.demandCommand(1, 'Specify a teams-phone subcommand.')
|
|
141
204
|
.strict(),
|
|
142
205
|
handler: () => {},
|
package/package.json
CHANGED