@velaro/cli 1.2.0 → 1.4.9
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 +161 -138
- package/bin/velaro.js +177 -62
- package/lib/api.js +91 -52
- package/lib/api.test.js +46 -0
- package/lib/banner.js +76 -0
- package/lib/commands/activity.js +133 -0
- package/lib/commands/acuity.js +66 -0
- package/lib/commands/agent.js +204 -50
- package/lib/commands/ai-config.js +193 -0
- package/lib/commands/ai-models.js +159 -0
- package/lib/commands/appointments.js +198 -0
- package/lib/commands/article.js +668 -388
- package/lib/commands/automation-draft.js +134 -0
- package/lib/commands/avatar.js +75 -0
- package/lib/commands/bigcommerce.js +50 -0
- package/lib/commands/billing-contacts.js +62 -0
- package/lib/commands/billing-email-preference.js +64 -0
- package/lib/commands/billing-subscription.js +265 -0
- package/lib/commands/billing.js +138 -0
- package/lib/commands/bot.js +141 -137
- package/lib/commands/bundle.js +168 -0
- package/lib/commands/calendly.js +62 -0
- package/lib/commands/callback.js +125 -0
- package/lib/commands/callrail.js +88 -0
- package/lib/commands/campaigns.js +44 -0
- package/lib/commands/case.js +102 -0
- package/lib/commands/check.js +163 -163
- package/lib/commands/compliance.js +229 -0
- package/lib/commands/conversation-efficiency.js +178 -0
- package/lib/commands/copilotstudio.js +114 -0
- package/lib/commands/coupon-grant.js +192 -0
- package/lib/commands/db.js +101 -0
- package/lib/commands/deployment.js +107 -107
- package/lib/commands/diagnostics.js +298 -0
- package/lib/commands/email-campaign.js +47 -0
- package/lib/commands/email-inbox.js +88 -0
- package/lib/commands/entitlement.js +176 -0
- package/lib/commands/env.js +45 -45
- package/lib/commands/feature-discovery.js +40 -0
- package/lib/commands/focus.js +278 -0
- package/lib/commands/index.js +38 -5
- package/lib/commands/ingest.js +31 -31
- package/lib/commands/inline-widget-config.js +126 -0
- package/lib/commands/integration.js +93 -0
- package/lib/commands/kb.js +450 -309
- package/lib/commands/login.js +86 -86
- package/lib/commands/logs.js +680 -0
- package/lib/commands/magento.js +210 -0
- package/lib/commands/mcp-key.js +188 -159
- package/lib/commands/migrate.js +134 -0
- package/lib/commands/migration-status.js +66 -0
- package/lib/commands/monday.js +137 -0
- package/lib/commands/netsuite.js +87 -0
- package/lib/commands/notifications.js +63 -0
- package/lib/commands/notion.js +70 -0
- package/lib/commands/ops.js +267 -173
- package/lib/commands/payment-recovery.js +170 -0
- package/lib/commands/pickup.js +172 -0
- package/lib/commands/pricing.js +132 -0
- package/lib/commands/product.js +55 -0
- package/lib/commands/recruiting.js +374 -0
- package/lib/commands/report.js +462 -0
- package/lib/commands/routing.js +304 -0
- package/lib/commands/rule.js +85 -85
- package/lib/commands/sharepoint.js +167 -0
- package/lib/commands/site-provision.js +68 -0
- package/lib/commands/site.js +62 -62
- package/lib/commands/sitesync.js +158 -0
- package/lib/commands/slack.js +64 -0
- package/lib/commands/squarespace.js +108 -0
- package/lib/commands/status.js +24 -24
- package/lib/commands/subscription.js +43 -0
- package/lib/commands/support.js +128 -0
- package/lib/commands/survey.js +216 -0
- package/lib/commands/team.js +144 -144
- package/lib/commands/teams-phone.js +131 -0
- package/lib/commands/teams.js +106 -0
- package/lib/commands/telephony.js +99 -0
- package/lib/commands/update.js +47 -47
- package/lib/commands/webflow.js +128 -0
- package/lib/commands/whoami.js +25 -22
- package/lib/commands/widget-container.js +152 -0
- package/lib/commands/woocommerce.js +240 -0
- package/lib/commands/workflow.js +233 -98
- package/lib/config.js +85 -83
- package/lib/kb-screenshot.js +320 -0
- package/lib/migrations/amscro.json +72 -0
- package/lib/migrations/azenta.json +68 -0
- package/lib/migrations/bluefire.json +49 -0
- package/lib/migrations/donaldson.json +75 -0
- package/lib/oauth.js +149 -135
- package/lib/run.js +21 -16
- package/lib/sharepoint-auth.js +138 -0
- package/lib/subscription.js +41 -39
- package/lib/track.js +35 -35
- package/lib/update-check.js +64 -64
- package/package.json +34 -19
- package/scripts/postinstall.js +12 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { messagingGet, messagingPost, messagingPut } from '../api.js';
|
|
2
|
+
|
|
3
|
+
// helpers
|
|
4
|
+
|
|
5
|
+
const RESET = '\x1b[0m';
|
|
6
|
+
const BOLD = '\x1b[1m';
|
|
7
|
+
const DIM = '\x1b[2m';
|
|
8
|
+
const CYN = '\x1b[36m';
|
|
9
|
+
const YEL = '\x1b[33m';
|
|
10
|
+
const GRN = '\x1b[32m';
|
|
11
|
+
const RED = '\x1b[31m';
|
|
12
|
+
|
|
13
|
+
function fmtDate(iso) {
|
|
14
|
+
if (!iso) return '-';
|
|
15
|
+
return new Date(iso).toLocaleString('en-US', { timeZone: 'America/Los_Angeles', dateStyle: 'short', timeStyle: 'short' });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const STATUS_NAMES = { 0: 'pending', 1: 'approved', 2: 'dismissed', 3: 'autosent' };
|
|
19
|
+
const STATUS_VALUES = { pending: 0, approved: 1, dismissed: 2, autosent: 3 };
|
|
20
|
+
const SOURCE_TYPE_NAMES = { 0: 'WorkflowComposedEmail', 1: 'FocusSuggestedReply', 2: 'CrmFieldUpdate' };
|
|
21
|
+
|
|
22
|
+
const STATUS_COLORS = {
|
|
23
|
+
pending: YEL,
|
|
24
|
+
approved: GRN,
|
|
25
|
+
dismissed: DIM,
|
|
26
|
+
autosent: CYN,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function statusLabel(status) {
|
|
30
|
+
const name = typeof status === 'number' ? (STATUS_NAMES[status] ?? String(status)) : String(status ?? '-');
|
|
31
|
+
const color = STATUS_COLORS[name] ?? RESET;
|
|
32
|
+
return color + name + RESET;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// list
|
|
36
|
+
|
|
37
|
+
const listCommand = {
|
|
38
|
+
command: 'list',
|
|
39
|
+
describe: 'List AI-composed drafts awaiting review (workflow-composed emails, Focus suggested replies, CRM field updates)',
|
|
40
|
+
builder: (y) => y
|
|
41
|
+
.option('status', {
|
|
42
|
+
type: 'string',
|
|
43
|
+
choices: ['pending', 'approved', 'dismissed', 'autosent'],
|
|
44
|
+
default: 'pending',
|
|
45
|
+
describe: 'Filter by draft status (default: pending)',
|
|
46
|
+
}),
|
|
47
|
+
handler: async (argv) => {
|
|
48
|
+
const statusNum = STATUS_VALUES[argv.status] ?? 0;
|
|
49
|
+
const data = await messagingGet(`/AutomationDrafts?status=${statusNum}`);
|
|
50
|
+
const drafts = Array.isArray(data?.drafts) ? data.drafts : [];
|
|
51
|
+
|
|
52
|
+
console.log(`\n${BOLD}-- Automation Drafts (${argv.status}) --------------------------------------${RESET}`);
|
|
53
|
+
if (!drafts.length) {
|
|
54
|
+
console.log(` ${DIM}No ${argv.status} drafts.${RESET}\n`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const d of drafts) {
|
|
59
|
+
const source = SOURCE_TYPE_NAMES[d.sourceType] ?? d.sourceType;
|
|
60
|
+
const confidence = d.confidenceScore != null ? `${Math.round(d.confidenceScore * 100)}%` : '-';
|
|
61
|
+
console.log(
|
|
62
|
+
` ${CYN}#${d.id}${RESET} ${statusLabel(d.status)} ${DIM}${source}${RESET} confidence: ${confidence} ${DIM}${fmtDate(d.createdAt)}${RESET}`
|
|
63
|
+
);
|
|
64
|
+
if (d.recipientEmail || d.subject) {
|
|
65
|
+
console.log(` To: ${d.recipientEmail ?? '-'} Subject: ${d.subject ?? '-'}`);
|
|
66
|
+
}
|
|
67
|
+
const preview = (d.editedContent ?? d.originalContent ?? '').replace(/\s+/g, ' ').slice(0, 140);
|
|
68
|
+
console.log(` ${DIM}${preview}${preview.length === 140 ? '...' : ''}${RESET}`);
|
|
69
|
+
}
|
|
70
|
+
console.log();
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// edit
|
|
75
|
+
|
|
76
|
+
const editCommand = {
|
|
77
|
+
command: 'edit <id> <content>',
|
|
78
|
+
describe: 'Edit the content of a pending automation draft before approving it',
|
|
79
|
+
builder: (y) => y
|
|
80
|
+
.positional('id', { type: 'number', describe: 'Draft ID' })
|
|
81
|
+
.positional('content', { type: 'string', describe: 'New draft content' }),
|
|
82
|
+
handler: async (argv) => {
|
|
83
|
+
await messagingPut(`/AutomationDrafts/${argv.id}/edit`, { EditedContent: argv.content });
|
|
84
|
+
console.log(`\n ${GRN}OK${RESET} Draft #${argv.id} updated.\n`);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// approve
|
|
89
|
+
|
|
90
|
+
const approveCommand = {
|
|
91
|
+
command: 'approve <id>',
|
|
92
|
+
describe: 'Approve a pending automation draft (sends the email now for workflow-composed emails)',
|
|
93
|
+
builder: (y) => y.positional('id', { type: 'number', describe: 'Draft ID' }),
|
|
94
|
+
handler: async (argv) => {
|
|
95
|
+
try {
|
|
96
|
+
await messagingPost(`/AutomationDrafts/${argv.id}/approve`);
|
|
97
|
+
console.log(`\n ${GRN}OK${RESET} Draft #${argv.id} approved.\n`);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
if (/503/.test(err.message)) {
|
|
100
|
+
console.log(`\n ${YEL}Email send temporarily suppressed. Try approve again shortly.${RESET}\n`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
console.error(` ${RED}FAIL${RESET} ${err.message}`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// dismiss
|
|
110
|
+
|
|
111
|
+
const dismissCommand = {
|
|
112
|
+
command: 'dismiss <id>',
|
|
113
|
+
describe: 'Dismiss a pending automation draft without sending/applying it',
|
|
114
|
+
builder: (y) => y.positional('id', { type: 'number', describe: 'Draft ID' }),
|
|
115
|
+
handler: async (argv) => {
|
|
116
|
+
await messagingPost(`/AutomationDrafts/${argv.id}/dismiss`);
|
|
117
|
+
console.log(`\n ${GRN}OK${RESET} Draft #${argv.id} dismissed.\n`);
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// top-level automation-draft command
|
|
122
|
+
|
|
123
|
+
export const automationDraftCommand = {
|
|
124
|
+
command: 'automation-draft <subcommand>',
|
|
125
|
+
describe: 'Review AI-composed drafts: workflow-composed emails, Focus suggested replies, CRM field updates (requires AiComposedEmails subscription)',
|
|
126
|
+
builder: (y) => y
|
|
127
|
+
.command(listCommand)
|
|
128
|
+
.command(editCommand)
|
|
129
|
+
.command(approveCommand)
|
|
130
|
+
.command(dismissCommand)
|
|
131
|
+
.demandCommand(1, 'Specify an automation-draft subcommand.')
|
|
132
|
+
.strict(),
|
|
133
|
+
handler: () => {},
|
|
134
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { request } from '../api.js';
|
|
2
|
+
import { runCommand } from '../run.js';
|
|
3
|
+
|
|
4
|
+
// ── velaro avatar ─────────────────────────────────────────────────────────────
|
|
5
|
+
// Superadmin commands to inspect/configure the AI Video Avatar (Simli/D-ID/HeyGen)
|
|
6
|
+
// for any customer site. Requires a Velaro admin JWT (site 1032).
|
|
7
|
+
//
|
|
8
|
+
// The customer-facing endpoints (GET/POST velaro-messaging /avatar/config) are
|
|
9
|
+
// JWT-scoped to the caller's own site only — these hit velaro-admin's own
|
|
10
|
+
// Avatar/superadmin/* endpoints instead, which can target any siteId.
|
|
11
|
+
|
|
12
|
+
export const avatarCommand = {
|
|
13
|
+
command: 'avatar <subcommand>',
|
|
14
|
+
describe: 'Manage the AI Video Avatar for any site (Velaro admin only)',
|
|
15
|
+
builder: yargs => yargs
|
|
16
|
+
.command({
|
|
17
|
+
command: 'get',
|
|
18
|
+
describe: 'Show AI Video Avatar config for a site',
|
|
19
|
+
builder: y => y.option('site', { type: 'number', demandOption: true, describe: 'Site ID' }),
|
|
20
|
+
handler: runCommand(async argv => {
|
|
21
|
+
const data = await request('GET', `/Avatar/superadmin/status?siteId=${argv.site}`);
|
|
22
|
+
if (!data.configured) {
|
|
23
|
+
console.log(`\nSite ${argv.site}: AI Video Avatar NOT configured (no saved config).\n`);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
console.log(`\nSite ${argv.site}: AI Video Avatar ${data.enabled ? 'ENABLED ✓' : 'configured but DISABLED'}`);
|
|
27
|
+
console.log(` Provider: ${data.providerSlug}`);
|
|
28
|
+
console.log(` Avatar ID: ${data.avatarId || '(provider default)'}`);
|
|
29
|
+
console.log(` Voice ID: ${data.voiceId || '(site default)'}`);
|
|
30
|
+
console.log(` Enabled: ${data.enabled ? 'yes' : 'no'}`);
|
|
31
|
+
console.log(` "Call me": ${data.showCallMeOption ? 'yes' : 'no'}`);
|
|
32
|
+
console.log(` Callback window: ${data.callbackConfirmWindowMinutes} min\n`);
|
|
33
|
+
}),
|
|
34
|
+
})
|
|
35
|
+
.command({
|
|
36
|
+
command: 'enable',
|
|
37
|
+
describe: 'Turn on (or update) the AI Video Avatar for a site',
|
|
38
|
+
builder: y => y
|
|
39
|
+
.option('site', { type: 'number', demandOption: true, describe: 'Site ID' })
|
|
40
|
+
.option('provider', { type: 'string', describe: 'Avatar provider slug (simli, d-id, heygen). Default on first setup: simli. Omit on a re-run to leave the existing provider untouched.' })
|
|
41
|
+
.option('bot', { type: 'string', describe: 'Reserved for a future per-bot override (not yet implemented on this endpoint)' })
|
|
42
|
+
.option('avatar-id',{ type: 'string', describe: 'Provider-specific avatar/face ID. Omit for provider default.' })
|
|
43
|
+
.option('voice', { type: 'string', describe: 'ElevenLabs voice ID override' })
|
|
44
|
+
.option('call-me', { type: 'boolean', describe: 'Show the "Call me instead" option. Omit on a re-run to leave it untouched.' }),
|
|
45
|
+
handler: runCommand(async argv => {
|
|
46
|
+
if (argv.bot) {
|
|
47
|
+
console.log(`\nNote: --bot is not yet supported by this endpoint — this sets the SITE-level avatar config only. Use the admin UI's per-bot override toggle for bot-specific control.\n`);
|
|
48
|
+
}
|
|
49
|
+
// Partial update: only send fields the caller actually passed, so re-running `enable`
|
|
50
|
+
// to change one field (e.g. just --voice) never silently resets provider/call-me back
|
|
51
|
+
// to a hardcoded default on an existing config.
|
|
52
|
+
const body = { enabled: true };
|
|
53
|
+
body.providerSlug = argv.provider ?? 'simli';
|
|
54
|
+
if (argv['avatar-id'] !== undefined) body.avatarId = argv['avatar-id'];
|
|
55
|
+
if (argv.voice !== undefined) body.voiceId = argv.voice;
|
|
56
|
+
if (argv['call-me'] !== undefined) body.showCallMeOption = argv['call-me'];
|
|
57
|
+
const data = await request('POST', `/Avatar/superadmin/config?siteId=${argv.site}`, body);
|
|
58
|
+
if (!data.success) throw new Error(data.error || 'Configure failed');
|
|
59
|
+
console.log(`\n✓ AI Video Avatar enabled for site ${argv.site}`);
|
|
60
|
+
console.log(` Provider: ${data.providerSlug}`);
|
|
61
|
+
console.log(` Avatar ID: ${data.avatarId || '(provider default)'}\n`);
|
|
62
|
+
}),
|
|
63
|
+
})
|
|
64
|
+
.command({
|
|
65
|
+
command: 'disable',
|
|
66
|
+
describe: 'Turn off the AI Video Avatar for a site (keeps the saved config)',
|
|
67
|
+
builder: y => y.option('site', { type: 'number', demandOption: true, describe: 'Site ID' }),
|
|
68
|
+
handler: runCommand(async argv => {
|
|
69
|
+
const data = await request('POST', `/Avatar/superadmin/config?siteId=${argv.site}`, { enabled: false });
|
|
70
|
+
if (!data.success) throw new Error(data.error || 'Update failed');
|
|
71
|
+
console.log(`\n✓ AI Video Avatar disabled for site ${argv.site}\n`);
|
|
72
|
+
}),
|
|
73
|
+
})
|
|
74
|
+
.demandCommand(1, 'Specify a subcommand: get | enable | disable'),
|
|
75
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* velaro bigcommerce — BigCommerce store management
|
|
3
|
+
*
|
|
4
|
+
* velaro bigcommerce refund-check <siteId> <orderId> Check whether an order is refundable
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { messagingGet } from '../api.js';
|
|
8
|
+
|
|
9
|
+
const GRN = '\x1b[32m';
|
|
10
|
+
const RED = '\x1b[31m';
|
|
11
|
+
const DIM = '\x1b[2m';
|
|
12
|
+
const BOLD = '\x1b[1m';
|
|
13
|
+
const RST = '\x1b[0m';
|
|
14
|
+
|
|
15
|
+
// ── refund-check ──────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
async function refundCheckHandler(argv) {
|
|
18
|
+
try {
|
|
19
|
+
const data = await messagingGet(`/SupportTools/sites/${argv.siteId}/bigcommerce/refund-check/${argv.orderId}`);
|
|
20
|
+
|
|
21
|
+
if (data.refundable) {
|
|
22
|
+
const method = data.methods?.[0];
|
|
23
|
+
console.log(`\n${GRN}${BOLD}REFUNDABLE${RST} via ${BOLD}${method?.providerId ?? 'unknown'}${RST} (offline: ${method?.offline ? 'true' : 'false'})\n`);
|
|
24
|
+
} else {
|
|
25
|
+
console.log(`\n${RED}${BOLD}NOT REFUNDABLE${RST} — ${data.message ?? data.error ?? 'unknown reason'}.`);
|
|
26
|
+
console.log(`${DIM}Recreate this order using an offline/manual payment method or the BigCommerce Test Payment Gateway (Settings > Payments), then re-check.${RST}\n`);
|
|
27
|
+
}
|
|
28
|
+
} catch (e) {
|
|
29
|
+
console.error(`\n${RED}✗ Refund check failed: ${e.message}${RST}\n`);
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── command export ────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
export const bigcommerceCommand = {
|
|
37
|
+
command: 'bigcommerce <subcommand>',
|
|
38
|
+
describe: 'BigCommerce store management',
|
|
39
|
+
builder: y => y
|
|
40
|
+
.command({
|
|
41
|
+
command: 'refund-check <siteId> <orderId>',
|
|
42
|
+
describe: 'Check whether a BigCommerce order currently has a refundable payment record',
|
|
43
|
+
builder: yy => yy
|
|
44
|
+
.positional('siteId', { describe: 'Site ID', type: 'number' })
|
|
45
|
+
.positional('orderId', { describe: 'BigCommerce order ID', type: 'number' }),
|
|
46
|
+
handler: refundCheckHandler,
|
|
47
|
+
})
|
|
48
|
+
.demandCommand(1, 'Specify a subcommand. Run "velaro bigcommerce --help" for options.'),
|
|
49
|
+
handler: () => {},
|
|
50
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// velaro billing-contacts — manage per-site billing contacts (CC on contracts)
|
|
2
|
+
import { request } from '../api.js';
|
|
3
|
+
|
|
4
|
+
async function listContacts() {
|
|
5
|
+
const contacts = await request('GET', '/BillingContacts');
|
|
6
|
+
if (!Array.isArray(contacts) || contacts.length === 0) {
|
|
7
|
+
console.log('No billing contacts on file.');
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
console.log(`\nBilling Contacts (${contacts.length})\n${'─'.repeat(50)}`);
|
|
11
|
+
for (const c of contacts) {
|
|
12
|
+
const cc = c.ccOnContracts ? ' [CC on contracts]' : '';
|
|
13
|
+
console.log(` [${c.id}] ${c.name} <${c.email}> ${c.role}${c.title ? ' · ' + c.title : ''}${cc}`);
|
|
14
|
+
if (c.phone) console.log(` ${c.phone}`);
|
|
15
|
+
}
|
|
16
|
+
console.log('');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function addContact(args) {
|
|
20
|
+
if (!args.name || !args.email) {
|
|
21
|
+
console.error('Usage: velaro billing-contacts add --name "Jane Smith" --email jane@company.com [--role Billing] [--title CFO] [--phone "+1 555 000 0000"] [--no-cc]');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
const body = {
|
|
25
|
+
name: args.name,
|
|
26
|
+
email: args.email,
|
|
27
|
+
role: args.role ?? 'Billing',
|
|
28
|
+
title: args.title,
|
|
29
|
+
phone: args.phone,
|
|
30
|
+
ccOnContracts: args.cc !== false,
|
|
31
|
+
};
|
|
32
|
+
const result = await request('POST', '/BillingContacts', body);
|
|
33
|
+
console.log(`✅ Added: ${result.name} <${result.email}> (${result.role}) ID=${result.id}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function removeContact(id) {
|
|
37
|
+
if (!id) {
|
|
38
|
+
console.error('Usage: velaro billing-contacts remove <id>');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
await request('DELETE', `/BillingContacts/${id}`);
|
|
42
|
+
console.log(`✅ Billing contact ${id} removed.`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const billingContactsCommand = {
|
|
46
|
+
command: 'billing-contacts <subcommand>',
|
|
47
|
+
describe: 'Manage billing contacts (people CC\'d on contracts)',
|
|
48
|
+
builder: (yargs) =>
|
|
49
|
+
yargs
|
|
50
|
+
.command('list', 'List billing contacts', {}, () => listContacts())
|
|
51
|
+
.command('add', 'Add a billing contact', {
|
|
52
|
+
name: { type: 'string', describe: 'Full name', demandOption: true },
|
|
53
|
+
email: { type: 'string', describe: 'Email address', demandOption: true },
|
|
54
|
+
role: { type: 'string', describe: 'Primary | Billing | Technical | Legal', default: 'Billing' },
|
|
55
|
+
title: { type: 'string', describe: 'Job title' },
|
|
56
|
+
phone: { type: 'string', describe: 'Phone number' },
|
|
57
|
+
cc: { type: 'boolean', describe: 'CC on contract emails', default: true },
|
|
58
|
+
}, (args) => addContact(args))
|
|
59
|
+
.command('remove <id>', 'Remove a billing contact by ID', {}, (args) => removeContact(args.id))
|
|
60
|
+
.demandCommand(1, 'Specify a subcommand: list | add | remove'),
|
|
61
|
+
handler: () => {},
|
|
62
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// velaro billing-email-preference — governed billing-notification email-type preferences
|
|
2
|
+
// (BillingEmailPreferenceController). Data model + UX only — no command here sends, or can send,
|
|
3
|
+
// a real customer email. Every type defaults OFF; see the "New feature requested by Alex"
|
|
4
|
+
// section of project_unified_billing_system_2026_08_30.md.
|
|
5
|
+
import { request } from '../api.js';
|
|
6
|
+
|
|
7
|
+
async function listPreferences(args) {
|
|
8
|
+
const url = args.siteId ? `/BillingEmailPreference?siteId=${args.siteId}` : '/BillingEmailPreference';
|
|
9
|
+
const result = await request('GET', url);
|
|
10
|
+
|
|
11
|
+
console.log(`\nLive today (informational — not governed here)\n${'-'.repeat(70)}`);
|
|
12
|
+
for (const s of result.liveToday) {
|
|
13
|
+
console.log(` ${s.displayName}`);
|
|
14
|
+
console.log(` ${s.description}`);
|
|
15
|
+
console.log(` source: ${s.sourceLocation}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
console.log(`\nGoverned types${args.siteId ? ` (site ${args.siteId})` : ' (system defaults)'}\n${'-'.repeat(70)}`);
|
|
19
|
+
for (const t of result.governed) {
|
|
20
|
+
const reach = t.isCurrentlyReachable ? '' : ' [not built yet — no event exists to trigger this]';
|
|
21
|
+
const setBy = args.siteId ? ` effective=${t.effective} (set by ${t.setBy})` : ` systemDefault=${t.systemDefault}`;
|
|
22
|
+
console.log(` [${t.notificationType}] ${t.displayName}${reach}${setBy}`);
|
|
23
|
+
}
|
|
24
|
+
console.log('');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function setPreference(notificationType, args) {
|
|
28
|
+
if (!args.siteId || args.enabled == null) {
|
|
29
|
+
console.error('Usage: velaro billing-email-preference set <notificationType> --site-id 100150 --enabled true|false');
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
const result = await request('PUT', `/BillingEmailPreference/${notificationType}?siteId=${args.siteId}`, {
|
|
33
|
+
isEnabled: args.enabled === 'true' || args.enabled === true,
|
|
34
|
+
});
|
|
35
|
+
console.log(`Site ${args.siteId} override for ${result.notificationType}: ${result.siteOverride}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function resetPreference(notificationType, args) {
|
|
39
|
+
if (!args.siteId) {
|
|
40
|
+
console.error('Usage: velaro billing-email-preference reset <notificationType> --site-id 100150');
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
const result = await request('DELETE', `/BillingEmailPreference/${notificationType}?siteId=${args.siteId}`);
|
|
44
|
+
console.log(result.reset ? `Site ${args.siteId} override for ${notificationType} reset to system default.` : `No override existed for ${notificationType}.`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const billingEmailPreferenceCommand = {
|
|
48
|
+
command: 'billing-email-preference <subcommand>',
|
|
49
|
+
describe: 'View and set billing email-notification-type preferences (data model + UX only — no real email is ever sent by this command)',
|
|
50
|
+
builder: (yargs) =>
|
|
51
|
+
yargs
|
|
52
|
+
.command('list', 'List live-today senders + governed types (with effective value if --site-id given)', {
|
|
53
|
+
'site-id': { type: 'number', desc: 'View effective value + override state for this site instead of system defaults only' },
|
|
54
|
+
}, (args) => listPreferences(args))
|
|
55
|
+
.command('set <notificationType>', 'Set a site-level override for one governed type', {
|
|
56
|
+
'site-id': { type: 'number' },
|
|
57
|
+
enabled: { type: 'string', desc: 'true or false' },
|
|
58
|
+
}, (args) => setPreference(args.notificationType, args))
|
|
59
|
+
.command('reset <notificationType>', "Reset a site's override back to the system default", {
|
|
60
|
+
'site-id': { type: 'number' },
|
|
61
|
+
}, (args) => resetPreference(args.notificationType, args))
|
|
62
|
+
.demandCommand(1, 'Specify a subcommand: list | set | reset'),
|
|
63
|
+
handler: () => {},
|
|
64
|
+
};
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// velaro billing-subscription — standalone, entitlement-decoupled recurring billing subscriptions
|
|
2
|
+
// (BillingSubscriptionController). See project_billing_subscription_target_decoupling.md — these
|
|
3
|
+
// records do not require a v20 SiteId and work for any product (v10/v20/none).
|
|
4
|
+
import { request } from '../api.js';
|
|
5
|
+
|
|
6
|
+
function money(cents) {
|
|
7
|
+
return `$${(cents / 100).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function createSubscription(args) {
|
|
11
|
+
if (!args.customer || !args.start || !args.end) {
|
|
12
|
+
console.error('Usage: velaro billing-subscription create --customer "Liberty Mutual" --start 2026-11-17 --end 2027-11-16 [--po 4500175509] [--cadence annual] [--payment-method cash] [--total-cents 7357397] [--net-terms 30] [--lead-days 30] [--enable-billing] [--stripe-customer-id cus_...]');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
const body = {
|
|
16
|
+
customerName: args.customer,
|
|
17
|
+
poNumber: args.po,
|
|
18
|
+
targetSystem: args.target,
|
|
19
|
+
targetAccountId: args.targetAccountId,
|
|
20
|
+
billingCadence: args.cadence ?? 'annual',
|
|
21
|
+
paymentMethod: args.paymentMethod ?? 'cash',
|
|
22
|
+
billingEnabled: !!args.enableBilling,
|
|
23
|
+
contractSignedDate: args.signed,
|
|
24
|
+
serviceStartDate: args.start,
|
|
25
|
+
serviceEndDate: args.end,
|
|
26
|
+
nextBillDate: args.nextBillDate,
|
|
27
|
+
invoiceLeadDays: args.leadDays ?? 30,
|
|
28
|
+
netTermsDays: args.netTerms ?? 30,
|
|
29
|
+
cancellationNoticeDays: args.cancellationNoticeDays ?? 30,
|
|
30
|
+
totalContractValueCents: args.totalCents ?? 0,
|
|
31
|
+
stripeCustomerId: args.stripeCustomerId,
|
|
32
|
+
notes: args.notes,
|
|
33
|
+
};
|
|
34
|
+
const result = await request('POST', '/api/BillingSubscription', body);
|
|
35
|
+
console.log(`Subscription created: ID=${result.id}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function attachStripeCustomer(id, args) {
|
|
39
|
+
if (!args.email && !args.stripeCustomerId) {
|
|
40
|
+
console.error('Usage: velaro billing-subscription attach-stripe-customer <id> --email billing@customer.com (looks up or creates a Stripe customer by email)');
|
|
41
|
+
console.error(' or: velaro billing-subscription attach-stripe-customer <id> --stripe-customer-id cus_xxx (attach a known Stripe customer ID directly)');
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
const body = { email: args.email, stripeCustomerId: args.stripeCustomerId };
|
|
45
|
+
const result = await request('POST', `/api/BillingSubscription/${id}/stripe-customer`, body);
|
|
46
|
+
console.log(`Subscription ${result.id} attached to Stripe customer ${result.stripeCustomerId}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function listSubscriptions() {
|
|
50
|
+
const rows = await request('GET', '/api/BillingSubscription');
|
|
51
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
52
|
+
console.log('No billing subscriptions on file.');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
console.log(`\nBilling Subscriptions (${rows.length})\n${'-'.repeat(60)}`);
|
|
56
|
+
for (const s of rows) {
|
|
57
|
+
console.log(` [${s.id}] ${s.customerName} PO=${s.poNumber ?? '(none)'} ${s.billingCadence}/${s.paymentMethod} billingEnabled=${s.billingEnabled} stripeCustomerId=${s.stripeCustomerId ?? '(none)'}`);
|
|
58
|
+
console.log(` ${s.serviceStartDate?.slice(0, 10)} .. ${s.serviceEndDate?.slice(0, 10)}`);
|
|
59
|
+
}
|
|
60
|
+
console.log('');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function daysFromNow(dateStr) {
|
|
64
|
+
const target = new Date(dateStr + 'T00:00:00Z');
|
|
65
|
+
const today = new Date(new Date().toISOString().slice(0, 10) + 'T00:00:00Z');
|
|
66
|
+
return Math.round((target - today) / 86400000);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function renewalsReport(args) {
|
|
70
|
+
const withinDays = args.withinDays ?? 9999;
|
|
71
|
+
const rows = await request('GET', '/api/BillingSubscription');
|
|
72
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
73
|
+
console.log('No billing subscriptions on file.');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const report = rows.map(s => {
|
|
77
|
+
const end = s.serviceEndDate?.slice(0, 10);
|
|
78
|
+
const daysToRenewal = end ? daysFromNow(end) : null;
|
|
79
|
+
const cancelDeadline = end ? new Date(new Date(end + 'T00:00:00Z').getTime() - s.cancellationNoticeDays * 86400000).toISOString().slice(0, 10) : null;
|
|
80
|
+
const daysToCancelDeadline = cancelDeadline ? daysFromNow(cancelDeadline) : null;
|
|
81
|
+
return { ...s, end, daysToRenewal, cancelDeadline, daysToCancelDeadline };
|
|
82
|
+
})
|
|
83
|
+
.filter(r => r.daysToRenewal == null || r.daysToRenewal <= withinDays)
|
|
84
|
+
.sort((a, b) => (a.daysToRenewal ?? Infinity) - (b.daysToRenewal ?? Infinity));
|
|
85
|
+
|
|
86
|
+
console.log(`\nRenewal Report${args.withinDays ? ` (within ${withinDays} days)` : ''}\n${'-'.repeat(90)}`);
|
|
87
|
+
for (const r of report) {
|
|
88
|
+
const canStillCancel = r.daysToCancelDeadline != null && r.daysToCancelDeadline >= 0;
|
|
89
|
+
const status = r.daysToCancelDeadline == null
|
|
90
|
+
? 'no end date on file'
|
|
91
|
+
: canStillCancel
|
|
92
|
+
? `can still cancel — ${r.daysToCancelDeadline}d left to give notice`
|
|
93
|
+
: `PAST cancellation window — will auto-renew (deadline was ${r.cancelDeadline})`;
|
|
94
|
+
console.log(` [${r.id}] ${r.customerName} renews ${r.end} (${r.daysToRenewal}d) ${status}`);
|
|
95
|
+
}
|
|
96
|
+
console.log('');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function getSubscription(id) {
|
|
100
|
+
const sub = await request('GET', `/api/BillingSubscription/${id}`);
|
|
101
|
+
console.log(JSON.stringify(sub, null, 2));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function generateInvoice(id, args) {
|
|
105
|
+
if (!args.lineDescription || args.amountCents == null) {
|
|
106
|
+
console.error('Usage: velaro billing-subscription invoice <id> --line-description "Annual" --amount-cents 7357397 [--pre-generate] [--po ...] [--invoice-date YYYY-MM-DD] [--due-date YYYY-MM-DD] [--service-start YYYY-MM-DD] [--service-end YYYY-MM-DD] [--notes ...]');
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
const body = {
|
|
110
|
+
poNumber: args.po,
|
|
111
|
+
servicePeriodStart: args.serviceStart,
|
|
112
|
+
servicePeriodEnd: args.serviceEnd,
|
|
113
|
+
invoiceDate: args.invoiceDate,
|
|
114
|
+
dueDate: args.dueDate,
|
|
115
|
+
isPreGenerated: !!args.preGenerate,
|
|
116
|
+
internalNotes: args.notes,
|
|
117
|
+
lineItems: [{ description: args.lineDescription, amountCents: Number(args.amountCents) }],
|
|
118
|
+
};
|
|
119
|
+
const invoice = await request('POST', `/api/BillingSubscription/${id}/invoice`, body);
|
|
120
|
+
console.log(`Invoice ${invoice.invoiceNumberFormatted} (id=${invoice.id}) — ${money(invoice.totalCents)}, status=${invoice.status}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function recordPayment(invoiceId, args) {
|
|
124
|
+
if (args.amountCents == null) {
|
|
125
|
+
console.error('Usage: velaro billing-subscription payment <invoiceId> --amount-cents 7357397 [--method cash|check|ach|wire|card] [--date YYYY-MM-DD] [--reference "..."] [--note "..."]');
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
const body = {
|
|
129
|
+
amountCents: Number(args.amountCents),
|
|
130
|
+
method: args.method ?? 'cash',
|
|
131
|
+
paymentDate: args.date,
|
|
132
|
+
reference: args.reference,
|
|
133
|
+
note: args.note,
|
|
134
|
+
};
|
|
135
|
+
const payment = await request('POST', `/api/BillingSubscription/invoice/${invoiceId}/payment`, body);
|
|
136
|
+
console.log(`Payment recorded: id=${payment.id}, amount=${money(payment.amountCents)}, method=${payment.method}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function listPayments(invoiceId) {
|
|
140
|
+
const result = await request('GET', `/api/BillingSubscription/invoice/${invoiceId}/payments`);
|
|
141
|
+
console.log(`\nPayments for invoice ${invoiceId}\n${'-'.repeat(50)}`);
|
|
142
|
+
for (const p of result.payments) {
|
|
143
|
+
console.log(` [${p.id}] ${money(p.amountCents)} ${p.method} ${p.paymentDate?.slice(0, 10)} ref=${p.reference ?? '(none)'}`);
|
|
144
|
+
}
|
|
145
|
+
console.log(`\nBalance: applied ${money(result.balance.appliedCents)} / total ${money(result.balance.totalCents)} — remaining ${money(result.balance.remainingCents)} — paidInFull=${result.balance.paidInFull}\n`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function qboStatus(invoiceId) {
|
|
149
|
+
const result = await request('GET', `/api/BillingSubscription/invoice/${invoiceId}/qbo-sync-status`);
|
|
150
|
+
console.log(`Invoice ${result.invoice.invoiceNumberFormatted}: QBO status = ${result.invoice.qboSyncStatus ?? '(none)'}`);
|
|
151
|
+
for (const p of result.payments) {
|
|
152
|
+
console.log(` Payment ${p.id}: QBO status = ${p.qboSyncStatus ?? '(none)'}, syncedAt = ${p.qboSyncedAt ?? '(never)'}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function provisionPlaceholderSite(id, args) {
|
|
157
|
+
if (!args.tier) {
|
|
158
|
+
console.error('Usage: velaro billing-subscription provision-placeholder-site <id> --tier Professional [--admin-email placeholder+staxbill-123@velaro.com] [--preview]');
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
const body = {
|
|
162
|
+
subscriptionTier: args.tier,
|
|
163
|
+
adminEmailOverride: args.adminEmail,
|
|
164
|
+
preview: !!args.preview,
|
|
165
|
+
};
|
|
166
|
+
const result = await request('POST', `/api/BillingSubscription/${id}/provision-placeholder-site`, body);
|
|
167
|
+
|
|
168
|
+
if (args.preview) {
|
|
169
|
+
console.log(`\nPreview: provision-placeholder-site for subscription ${result.subscriptionId}\n${'-'.repeat(60)}`);
|
|
170
|
+
console.log(` Company: ${result.companyName}`);
|
|
171
|
+
console.log(` Admin: ${result.adminEmail}`);
|
|
172
|
+
console.log(` Tier: ${result.subscriptionTier}`);
|
|
173
|
+
if (result.existingSiteId != null) {
|
|
174
|
+
console.log(` SiteId: ${result.existingSiteId} (already allocated -- would be REUSED, not re-allocated)`);
|
|
175
|
+
} else {
|
|
176
|
+
console.log(` SiteId: (would allocate a new placeholder id -- not shown here, preview never consumes a sequence value)`);
|
|
177
|
+
}
|
|
178
|
+
console.log(' No writes made (--preview).\n');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (result.provisioningSucceeded) {
|
|
183
|
+
console.log(`\x1b[32mOK: placeholder site ${result.siteId} provisioned for subscription ${result.subscriptionId}\x1b[0m`);
|
|
184
|
+
console.log(` Company: ${result.companyName}`);
|
|
185
|
+
console.log(` Admin: ${result.adminEmail}`);
|
|
186
|
+
console.log(` Tier: ${result.subscriptionTier}`);
|
|
187
|
+
if (result.wasAlreadyAllocated) console.log(` (SiteId was already allocated by a prior call -- resumed, not re-allocated.)`);
|
|
188
|
+
} else {
|
|
189
|
+
console.error(`\x1b[31mSiteId ${result.siteId} is allocated but provisioning FAILED: ${result.provisioningError}\x1b[0m`);
|
|
190
|
+
console.error(` Check site ${result.siteId} manually, or retry this same command -- it will resume against the same SiteId, not allocate a new one.`);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function recognitionSchedule(invoiceId) {
|
|
196
|
+
const rows = await request('GET', `/api/BillingSubscription/invoice/${invoiceId}/recognition-schedule`);
|
|
197
|
+
console.log(`\nRevenue recognition schedule for invoice ${invoiceId}\n${'-'.repeat(50)}`);
|
|
198
|
+
for (const r of rows) {
|
|
199
|
+
console.log(` ${r.recognitionMonth.slice(0, 7)} ${money(r.amountCents)} [${r.status}]`);
|
|
200
|
+
}
|
|
201
|
+
console.log(`\nTotal: ${money(rows.reduce((sum, r) => sum + r.amountCents, 0))}\n`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export const billingSubscriptionCommand = {
|
|
205
|
+
command: 'billing-subscription <subcommand>',
|
|
206
|
+
describe: 'Manage standalone billing subscriptions, invoices, payments, and QBO dry-run sync status',
|
|
207
|
+
builder: (yargs) =>
|
|
208
|
+
yargs
|
|
209
|
+
.command('list', 'List all billing subscriptions', {}, () => listSubscriptions())
|
|
210
|
+
.command('get <id>', 'Get a subscription by ID (with invoices)', {}, (args) => getSubscription(args.id))
|
|
211
|
+
.command('create', 'Create a new billing subscription', {
|
|
212
|
+
customer: { type: 'string' },
|
|
213
|
+
po: { type: 'string' },
|
|
214
|
+
target: { type: 'string' },
|
|
215
|
+
'target-account-id': { type: 'string' },
|
|
216
|
+
cadence: { type: 'string', default: 'annual' },
|
|
217
|
+
'payment-method': { type: 'string', default: 'cash' },
|
|
218
|
+
signed: { type: 'string' },
|
|
219
|
+
start: { type: 'string' },
|
|
220
|
+
end: { type: 'string' },
|
|
221
|
+
'next-bill-date': { type: 'string' },
|
|
222
|
+
'lead-days': { type: 'number' },
|
|
223
|
+
'net-terms': { type: 'number' },
|
|
224
|
+
'cancellation-notice-days': { type: 'number' },
|
|
225
|
+
'total-cents': { type: 'number' },
|
|
226
|
+
notes: { type: 'string' },
|
|
227
|
+
'enable-billing': { type: 'boolean', default: false },
|
|
228
|
+
'stripe-customer-id': { type: 'string' },
|
|
229
|
+
}, (args) => createSubscription(args))
|
|
230
|
+
.command('attach-stripe-customer <id>', 'Look up (by email) or create a Stripe customer and permanently attach it to a subscription', {
|
|
231
|
+
email: { type: 'string' },
|
|
232
|
+
'stripe-customer-id': { type: 'string' },
|
|
233
|
+
}, (args) => attachStripeCustomer(args.id, args))
|
|
234
|
+
.command('invoice <id>', 'Generate (or pre-generate) an invoice for a subscription', {
|
|
235
|
+
'line-description': { type: 'string' },
|
|
236
|
+
'amount-cents': { type: 'number' },
|
|
237
|
+
po: { type: 'string' },
|
|
238
|
+
'invoice-date': { type: 'string' },
|
|
239
|
+
'due-date': { type: 'string' },
|
|
240
|
+
'service-start': { type: 'string' },
|
|
241
|
+
'service-end': { type: 'string' },
|
|
242
|
+
'pre-generate': { type: 'boolean', default: false },
|
|
243
|
+
notes: { type: 'string' },
|
|
244
|
+
}, (args) => generateInvoice(args.id, args))
|
|
245
|
+
.command('provision-placeholder-site <id>', 'Allocate (or resume) a placeholder v20 SiteId and provision a real placeholder site under it via messaging', {
|
|
246
|
+
tier: { type: 'string', describe: 'Subscription tier (required, no default -- e.g. Starter, Professional, Enterprise)' },
|
|
247
|
+
'admin-email': { type: 'string', describe: 'Override the default placeholder+staxbill-<id>@velaro.com admin email' },
|
|
248
|
+
preview: { type: 'boolean', default: false, describe: 'Show what would happen without allocating or calling messaging' },
|
|
249
|
+
}, (args) => provisionPlaceholderSite(args.id, args))
|
|
250
|
+
.command('recognition-schedule <invoiceId>', 'Show the monthly revenue-recognition schedule for an invoice', {}, (args) => recognitionSchedule(args.invoiceId))
|
|
251
|
+
.command('payment <invoiceId>', 'Record a payment against an invoice', {
|
|
252
|
+
'amount-cents': { type: 'number' },
|
|
253
|
+
method: { type: 'string', default: 'cash' },
|
|
254
|
+
date: { type: 'string' },
|
|
255
|
+
reference: { type: 'string' },
|
|
256
|
+
note: { type: 'string' },
|
|
257
|
+
}, (args) => recordPayment(args.invoiceId, args))
|
|
258
|
+
.command('payments <invoiceId>', 'List payments + balance for an invoice', {}, (args) => listPayments(args.invoiceId))
|
|
259
|
+
.command('qbo-status <invoiceId>', 'Show QBO dry-run sync status for an invoice and its payments', {}, (args) => qboStatus(args.invoiceId))
|
|
260
|
+
.command('renewals', 'List subscriptions by upcoming renewal date, flagging who is still cancellable vs past their cancellation-notice deadline', {
|
|
261
|
+
'within-days': { type: 'number', desc: 'Only show renewals within this many days (default: all)' },
|
|
262
|
+
}, (args) => renewalsReport(args))
|
|
263
|
+
.demandCommand(1, 'Specify a subcommand: list | get | create | attach-stripe-customer | invoice | provision-placeholder-site | recognition-schedule | payment | payments | qbo-status | renewals'),
|
|
264
|
+
handler: () => {},
|
|
265
|
+
};
|