@go-labs-sg/bb 1.20.0 → 2.0.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 +142 -43
- package/command-manifest.json +6606 -0
- package/command-reference.md +1153 -0
- package/dist/api-client.js +44 -8
- package/dist/cli-trace.js +6 -1
- package/dist/commands.js +183 -23
- package/dist/index.js +389 -44
- package/dist/load-env.js +1 -1
- package/dist/parse-mutation-payload.js +22 -9
- package/dist/registry/generate-command-artifacts.js +36 -0
- package/dist/registry/index.js +583 -0
- package/dist/runtime/confirmation.js +76 -0
- package/dist/runtime/error.js +143 -0
- package/dist/runtime/index.js +6 -0
- package/dist/runtime/output.js +36 -0
- package/dist/runtime/process-runtime.js +28 -0
- package/dist/runtime/sanitize.js +50 -0
- package/dist/runtime/session.js +50 -0
- package/dist/runtime/types.js +1 -0
- package/package.json +13 -19
- package/role-aware-agent-guide.md +169 -0
package/dist/index.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import "./load-env.js";
|
|
3
|
-
import {
|
|
3
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { BUDGET_BUILDER_API_BASE_URL, configureBudgetBuilderApiBaseUrl, DEFAULT_BUDGET_BUILDER_API_BASE_URL, requireApiKey, } from "./api-client.js";
|
|
4
7
|
import { consumeCliQuietFlags, logCliAction, sanitizeFlagsForTrace, setCliQuiet, shouldLogCliActions, } from "./cli-trace.js";
|
|
5
|
-
import { addBudgetItems, approveBill, approveBudget, approveCustomerInvoice, approveQuotation, approveSupplier, checkCustomerInvoiceReadiness, checkProjectReconciliation, cleanupStagedBillAttachments, cleanupStagedQuotationAttachments, completeProject, createApiKeyForUser, createBillApproval, createBillFromPayload, createBudgetApproval, createBudgetCategory, createBudgetFromPayload, createCompany, createContactPersonFromPayload, createCustomerInvoice, createEstimate, createItemCategory, createItemFromPayload, createPlaceholderBillForBudgetItem, createProject, createQuotationFromPayload, createSupplierCertification, createSupplierFromPayload, createSupplierPaymentMethod, createSupplierRoleOption, createSupplierTagOption, createUser, deleteBillById, deleteBudgetById, deleteBudgetCategory, deleteBudgetCommission, deleteBudgetDiscount, deleteCompanyById, deleteCustomerInvoice, deleteItemById, deleteItemCategoriesByIds, deleteProjectById, deleteQuotationById, deleteSuppliersByIds, discardCreatingCustomerInvoice, downloadCustomerInvoicePdf, downloadQuotationPdf, getApprovedBudgets, getBillAttachments, getBillDetails, getBudget, getBudgetCategories, getBudgetCategoryBenchmarks, getBudgetDetails, getBudgetItemsOnly, getBudgetVersions, getCompany, getCustomerInvoice, getDashboard, getErrorMetrics, getEstimatePerformance, getFinancialOverview, getItem, getItemPricingHistory, getMonthlyMetrics, getProject, getQuotationDetails, getRecentErrors, getSupplierAnalytics, getSupplierDetails, getSupplierPricingHistory, getSystemOverview, getUserPerformance, importQuickBooksProjectId, listApiKeysForUser, listApprovals, listBills, listBudgets, listCompanies, listContacts, listCustomerInvoices, listIntegrationOperations, listItemCategories, listItems, listProjects, listQuotations, listSuppliers, listUsers, markBudgetWonWithProof, patchBillInvoiceNumber, patchBillPayment, reactivateSuppliersByIds, reconcileProject, rejectBill, rejectBudget, rejectCustomerInvoice, rejectQuotation, rejectSupplier, removeBudgetItem, renameBudgetVersion, reorderBudgetItemsCli, restoreBudgetVersion, retryIntegrationOperation, revokeApiKeyForUser, sendEstimateToContactPersonFromPayload, setBudgetItemsNotUtilized, stageBillAttachmentsFromPaths, submitQuotation, syncCustomerInvoice, updateBillFromPayload, updateBillPaymentEvidenceFromPayload, updateBillStatus, updateBudgetCategory, updateBudgetCommissionFromPayload, updateBudgetDiscountFromPayload, updateBudgetFromPayload, updateBudgetItem, updateBudgetItemSupplierCli, updateBudgetStatus, updateCompanyFromPayload, updateContactPersonFromPayload, updateItemCategory, updateItemFromPayload, updateProjectFromPayload, updateProjectStatus, updateQuotationFromPayload, updateSupplierFromPayload, uploadBillAttachmentFromPath, uploadBillAttachmentsFromPaths, uploadBillDocumentsFromPaths, uploadBudgetAttachmentFromPath, uploadQuotationAttachmentFromPath, validateBillSelectionFromPayload, voidCustomerInvoice, } from "./commands.js";
|
|
8
|
+
import { addBudgetItems, approveBill, approveBudget, approveCustomerInvoice, approveQuotation, approveSupplier, checkCustomerInvoiceReadiness, checkProjectReconciliation, cleanupStagedBillAttachments, cleanupStagedQuotationAttachments, completeProject, createApiKeyForUser, createBillApproval, createBillFromPayload, createBudgetApproval, createBudgetCategory, createBudgetFromPayload, createCompany, createContactPersonFromPayload, createCustomerInvoice, createEstimate, createItemCategory, createItemFromPayload, createPlaceholderBillForBudgetItem, createProject, createQuotationFromPayload, createSupplierCertification, createSupplierFromPayload, createSupplierPaymentMethod, createSupplierRoleOption, createSupplierTagOption, createUser, deleteBillById, deleteBudgetById, deleteBudgetCategory, deleteBudgetCommission, deleteBudgetDiscount, deleteCompanyById, deleteCustomerInvoice, deleteItemById, deleteItemCategoriesByIds, deleteProjectById, deleteQuotationById, deleteSuppliersByIds, discardCreatingCustomerInvoice, downloadCustomerInvoicePaymentProof, downloadCustomerInvoicePdf, downloadQuotationPdf, getApprovedBudgets, getBillAttachments, getBillDetails, getBudget, getBudgetCategories, getBudgetCategoryBenchmarks, getBudgetDetails, getBudgetItemsOnly, getBudgetVersions, getCompany, getCustomerInvoice, getCustomerInvoiceEmailContext, getDashboard, getErrorMetrics, getEstimatePerformance, getFinancialOverview, getItem, getItemPricingHistory, getMonthlyMetrics, getProject, getQuotationDetails, getRecentErrors, getSupplierAnalytics, getSupplierDetails, getSupplierPricingHistory, getSystemOverview, getUserPerformance, importQuickBooksProjectId, listApiKeysForUser, listApprovals, listBills, listBudgets, listCompanies, listContacts, listCustomerInvoices, listEligibleCustomerInvoiceBudgets, listIntegrationOperations, listItemCategories, listItems, listProjects, listQuotations, listSuppliers, listUsers, markBudgetWonWithProof, markCustomerInvoicePaidFromPath, patchBillInvoiceNumber, patchBillPayment, reactivateSuppliersByIds, reconcileProject, rejectBill, rejectBudget, rejectCustomerInvoice, rejectQuotation, rejectSupplier, removeBudgetItem, renameBudgetVersion, reorderBudgetItemsCli, restoreBudgetVersion, retryIntegrationOperation, revokeApiKeyForUser, sendCustomerInvoiceToContactPersonFromPayload, sendEstimateToContactPersonFromPayload, setBudgetItemsNotUtilized, stageBillAttachmentsFromPaths, submitQuotation, syncCustomerInvoice, updateBillFromPayload, updateBillPaymentEvidenceFromPayload, updateBillStatus, updateBudgetCategory, updateBudgetCommissionFromPayload, updateBudgetDiscountFromPayload, updateBudgetFromPayload, updateBudgetItem, updateBudgetItemSupplierCli, updateBudgetStatus, updateCompanyFromPayload, updateContactPersonFromPayload, updateItemCategory, updateItemFromPayload, updateProjectFromPayload, updateProjectStatus, updateQuotationFromPayload, updateSupplierFromPayload, uploadBillAttachmentFromPath, uploadBillAttachmentsFromPaths, uploadBillDocumentsFromPaths, uploadBudgetAttachmentFromPath, uploadQuotationAttachmentFromPath, validateBillSelectionFromPayload, voidCustomerInvoice, whoAmI, } from "./commands.js";
|
|
6
9
|
import { getFlag, parseArgs } from "./parse-args.js";
|
|
7
10
|
import { billStatusesForUpdateHelp, budgetStatusesForHelp, isBudgetStatusUpdate, parseApprovalTypeFlag, parseBillStatusForUpdate, parseCommaSeparatedBillStatuses, parseCommaSeparatedBudgetStatuses, parseCommaSeparatedCustomerInvoiceStatuses, parseCommaSeparatedIds, parseCommaSeparatedQuotationStatuses, parseCommaSeparatedSupplierStatuses, parseOptionalBillListSortBy, parseOptionalBillListSortDir, parseOptionalDashboardRole, parseOptionalDeals, parseOptionalErrorSeverity, parseOptionalErrorStatus, parseOptionalExtendedProjectStatus, parseOptionalFinancialRole, parseOptionalSupplierAnalyticsTimeFrame, parseOptionalTimeFrame, parseProjectStatusForUpdate, parseUserRole, projectStatusesForHelp, userRolesForHelp, } from "./parse-cli-enums.js";
|
|
8
11
|
import { parseJsonFlag, parseOptionalNumber as parseOptNum, } from "./parse-json-flag.js";
|
|
12
|
+
import { createCompletionScript, createHumanHelp, resolveCommand, } from "./registry/index.js";
|
|
13
|
+
import { CliRuntimeError, clearActiveCliSession, confirmCurrentCommand, createProcessRuntime, emitCommandError, emitCommandResult, setActiveCliSession, } from "./runtime/index.js";
|
|
9
14
|
function parsePositiveIntFlag(value, flagName) {
|
|
10
15
|
if (value === undefined)
|
|
11
16
|
return undefined;
|
|
@@ -138,15 +143,24 @@ function parseDashboardQueryFlags(flags) {
|
|
|
138
143
|
endDate: getFlag(flags, "endDate"),
|
|
139
144
|
};
|
|
140
145
|
}
|
|
141
|
-
function
|
|
146
|
+
function printLegacyHelp(runtime) {
|
|
142
147
|
const help = `
|
|
143
148
|
bb — Budget Builder CLI for AI agents (parity with MCP tools)
|
|
144
149
|
|
|
145
150
|
Usage: bb <command> [options] [args]
|
|
146
151
|
|
|
152
|
+
Identity: bb version — print the installed CLI package version and API target as JSON.
|
|
153
|
+
|
|
147
154
|
Global: --quiet | -q | BB_CLI_QUIET=1 — hide action logs (default: log command + each tRPC call to stderr; stdout JSON unchanged).
|
|
148
155
|
|
|
149
|
-
Auth:
|
|
156
|
+
Auth: Create a key under API Keys in Budget Builder and set BB_API_KEY in the environment.
|
|
157
|
+
|
|
158
|
+
Authorization: Run bb whoami before choosing commands. bb help lists the full
|
|
159
|
+
catalog for every role; it does not prove the active key may call a command.
|
|
160
|
+
The API evaluates whoami.role (USER, INSIDE_SALES, LEAD, ACCOUNTING_TEAM, or
|
|
161
|
+
ADMIN) together with resource ownership/project assignment, current state, and
|
|
162
|
+
pending-approver assignment. On FORBIDDEN, stop and report the identity, role,
|
|
163
|
+
command, and target; do not switch keys or try a lower-level procedure.
|
|
150
164
|
|
|
151
165
|
Sensitive workflow changes: approval decisions/requests, entity status updates,
|
|
152
166
|
estimate emails, customer invoices, integration retries, mark-budget-won,
|
|
@@ -170,7 +184,7 @@ Budgets
|
|
|
170
184
|
ESTIMATE_ACCEPTED requires win proof, auto-marks PITCH or LOST projects WON, requires --projectManagerId when the project has no PM, and auto-creates the Asana project/section when enabled.
|
|
171
185
|
ESTIMATE_REJECTED requires --projectStatusOnCommercialRejection when rejecting the only accepted/closed budget on a commercial project.
|
|
172
186
|
status: ${budgetStatusesForHelp.join(", ")}
|
|
173
|
-
|
|
187
|
+
mark-budget-won <budgetId> <filePath> [--projectManagerId <id>] [--invoiceSettings <json>] Upload signed quote/PO proof and set status to ESTIMATE_ACCEPTED; first acceptance requires invoice settings when customer invoicing is enabled
|
|
174
188
|
create-budget --payload '<json>' (budget.createBudget; Asana deal card is on the project)
|
|
175
189
|
update-budget --payload '<json>' (budget.updateBudget; must include id)
|
|
176
190
|
delete-budget <budgetId>
|
|
@@ -198,10 +212,10 @@ Bills
|
|
|
198
212
|
list-bills [--projectId] [--budgetId] [--status CSV] [--search <text>] [--isClaimable true|false] [--createdByIds <csv>] [--sortBy createdAt|amount|status] [--sortDir asc|desc] [--page] [--pageSize]
|
|
199
213
|
list-claims same flags as list-bills; only reimbursable claims (ignores --isClaimable)
|
|
200
214
|
isClaimable differentiates the shared bill/claim records: false = bill, true = claim.
|
|
201
|
-
|
|
215
|
+
Non-legacy supplier bills from 1 Jul 2026 00:00 SGT require approved quotation coverage for every selected line item, including already-paid bills.
|
|
202
216
|
create-bill-approval <billId> (also sends approval request emails)
|
|
203
217
|
create-bill --payload '<json>' (bill.create; payload.isClaimable false = bill, true = claim)
|
|
204
|
-
validate-bill-selection --payload '<json>' Check projectId, supplierId, budgetItemIds
|
|
218
|
+
validate-bill-selection --payload '<json>' Check projectId, supplierId, and budgetItemIds before creation; alreadyPaid never bypasses quotation checks.
|
|
205
219
|
stage-bill-attachment <projectId> <filePath...> [--file <path>] [--files <csv>] Upload files before create-bill; returns attachment JSON for attachments/paymentProofAttachments
|
|
206
220
|
cleanup-staged-bill-attachments <projectId> --keys <csv> Delete unattached staged bill/claim uploads.
|
|
207
221
|
update-bill --payload '<json>' (bill.update; must include id)
|
|
@@ -235,16 +249,23 @@ Quotations
|
|
|
235
249
|
|
|
236
250
|
Customer invoices
|
|
237
251
|
check-customer-invoice-readiness <budgetId>
|
|
238
|
-
list-customer-
|
|
252
|
+
list-eligible-customer-invoice-budgets <projectId> Accepted budgets that are ready and still have invoice coverage remaining.
|
|
253
|
+
list-customer-invoices [budgetId] [--projectId <id>] [--companyIds CSV] [--projectIds CSV] [--budgetIds CSV] [--createdByIds CSV] [--status CSV] [--search] [--sortBy createdAt|status|totalNetAmount|approvalExpiresAt] [--sortDir asc|desc] [--page] [--perPage]
|
|
254
|
+
No scope flag lists invoices globally. --projectId uses the project scope. A positional budgetId is a backward-compatible shortcut for --budgetIds.
|
|
239
255
|
get-customer-invoice <batchId>
|
|
256
|
+
get-customer-invoice-email-context <batchId> Contact, required CC, budget, and project context for composing an invoice email.
|
|
240
257
|
create-customer-invoice --payload '<json>' budgetId + one split with label, percentage, and dueDate.
|
|
241
258
|
discard-customer-invoice <batchId> Discard a reserved CREATING batch with no created QBO invoices.
|
|
242
259
|
delete-customer-invoice <batchId> Delete QBO invoices and the local batch.
|
|
243
260
|
void-customer-invoice <batchId> Void the invoice batch in QuickBooks.
|
|
244
|
-
approve-customer-invoice <batchId> Admin approval; notifies the creator.
|
|
261
|
+
approve-customer-invoice <batchId> Admin approval; rejects voided invoices, notifies the creator, and closes the estimate at cumulative 100% coverage.
|
|
245
262
|
reject-customer-invoice <batchId> --reason <text> Admin rejection; voids QBO invoices and notifies the creator.
|
|
263
|
+
send-customer-invoice-to-contact-person --payload '<json>' Same composer workflow as web; requires invoiceId, to, cc, replyTo, subject, HTML content, and HTML signature.
|
|
264
|
+
mark-customer-invoice-paid <invoiceId> --paymentDate YYYY-MM-DD --paymentProof <path> [--paymentReference <text>]
|
|
265
|
+
Admin-only. Uploads proof, creates a QBO Payment for the live balance when needed, attaches proof in QBO when possible, and records payment history in BB.
|
|
266
|
+
download-customer-invoice-payment-proof <invoiceId> [--output <path>]
|
|
246
267
|
download-customer-invoice-pdf <invoiceId> [--output <path>]
|
|
247
|
-
sync-customer-invoice <invoiceId> Refresh local status and balance from QuickBooks.
|
|
268
|
+
sync-customer-invoice <invoiceId> Refresh local status and balance from QuickBooks; paid invoices become PAID, reversals restore delivery/approval, and QBO voids can reopen an estimate.
|
|
248
269
|
|
|
249
270
|
Approvals
|
|
250
271
|
list-approvals | get-pending-approvals [--type budget|supplier|bill|quotation|customer_invoice|all]
|
|
@@ -298,30 +319,31 @@ Suppliers & items
|
|
|
298
319
|
update-item --payload '<json>' (item.updateItem; must include id)
|
|
299
320
|
delete-item <id>
|
|
300
321
|
get-supplier-details <supplierId>
|
|
301
|
-
get-supplier-analytics [--name] [--page] [--perPage] [--timeFrame ALL|LAST_YEAR|…]
|
|
322
|
+
get-supplier-analytics [--name] [--page] [--perPage] [--timeFrame ALL|LAST_YEAR|…] (admin)
|
|
302
323
|
list-items [--name] [--page] [--perPage]
|
|
303
324
|
get-item <id>
|
|
304
325
|
list-item-categories [--page] [--perPage]
|
|
305
|
-
create-item-category --name
|
|
306
|
-
update-item-category --id --name
|
|
326
|
+
create-item-category --name (admin)
|
|
327
|
+
update-item-category --id --name (admin)
|
|
307
328
|
delete-item-categories --ids <csv> (itemCategory.deleteItemCategories; admin; empty categories only)
|
|
308
329
|
|
|
309
330
|
Dashboard & users
|
|
331
|
+
whoami Show the user identity and role for the active API key.
|
|
310
332
|
list-users
|
|
311
333
|
create-user --email <email> [--name <name>] [--role ${userRolesForHelp.join("|")}] (admin; API-only service identity with no Google sign-in)
|
|
312
|
-
create-api-key --
|
|
313
|
-
list-api-keys [--userId <userId>] (
|
|
314
|
-
revoke-api-key <apiKeyId> [--userId <userId>] (
|
|
315
|
-
get-user-performance [--userId]
|
|
316
|
-
get-dashboard [--userId] [--role BD|CREATOR|INSIDE_SALES|ALL] [--deals ALL|SUCCESSFUL|LOST] [--timeFrame] [--startDate] [--endDate]
|
|
317
|
-
get-monthly-metrics [same optional flags as get-dashboard] (dashboard.getMonthlyMetrics)
|
|
318
|
-
get-system-overview [same optional flags as get-dashboard] (dashboard.getSystemOverview)
|
|
319
|
-
get-estimate-performance [same optional flags as get-dashboard] (dashboard.getEstimatePerformance)
|
|
320
|
-
get-financial-overview [same optional flags as dashboard; role uses BudgetRole enum]
|
|
334
|
+
create-api-key --name <label> [--userId <userId>] (defaults to the caller; admin required for another user; raw key is shown once)
|
|
335
|
+
list-api-keys [--userId <userId>] (defaults to the caller; admin required for another user)
|
|
336
|
+
revoke-api-key <apiKeyId> [--userId <userId>] (defaults to the caller; admin required for another user)
|
|
337
|
+
get-user-performance [--userId] (admin)
|
|
338
|
+
get-dashboard [--userId] [--role BD|CREATOR|INSIDE_SALES|ALL] [--deals ALL|SUCCESSFUL|LOST] [--timeFrame] [--startDate] [--endDate] (admin)
|
|
339
|
+
get-monthly-metrics [same optional flags as get-dashboard] (admin; dashboard.getMonthlyMetrics)
|
|
340
|
+
get-system-overview [same optional flags as get-dashboard] (admin; dashboard.getSystemOverview)
|
|
341
|
+
get-estimate-performance [same optional flags as get-dashboard] (admin; dashboard.getEstimatePerformance)
|
|
342
|
+
get-financial-overview [same optional flags as dashboard; role uses BudgetRole enum] (admin)
|
|
321
343
|
|
|
322
344
|
Errors
|
|
323
|
-
get-recent-errors [--page] [--perPage] [--severity] [--status]
|
|
324
|
-
get-error-metrics
|
|
345
|
+
get-recent-errors [--page] [--perPage] [--severity] [--status] (admin)
|
|
346
|
+
get-error-metrics (admin)
|
|
325
347
|
|
|
326
348
|
Automation (admin)
|
|
327
349
|
list-integration-operations [--destination] [--status PENDING|PROCESSING|FAILED|COMPLETED] [--page] [--perPage]
|
|
@@ -343,21 +365,251 @@ Note: MCP also exposes a Prisma schema resource; the CLI has no equivalent.
|
|
|
343
365
|
|
|
344
366
|
Not covered vs MCP get_budget: "get-budget" also fetches line items in one call.
|
|
345
367
|
`.trim();
|
|
346
|
-
|
|
368
|
+
runtime.stdout.write(`${help}\n`);
|
|
347
369
|
}
|
|
370
|
+
const getCliPackageMetadata = () => {
|
|
371
|
+
const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
372
|
+
if (!packageJson.name || !packageJson.version) {
|
|
373
|
+
throw new Error("Could not read Budget Builder CLI package metadata.");
|
|
374
|
+
}
|
|
375
|
+
return { name: packageJson.name, version: packageJson.version };
|
|
376
|
+
};
|
|
377
|
+
const getVersionData = () => {
|
|
378
|
+
const metadata = getCliPackageMetadata();
|
|
379
|
+
return {
|
|
380
|
+
...metadata,
|
|
381
|
+
apiBaseUrl: BUDGET_BUILDER_API_BASE_URL,
|
|
382
|
+
};
|
|
383
|
+
};
|
|
348
384
|
/** MCP registers tools with snake_case; CLI commands are kebab-case. */
|
|
349
385
|
function normalizeCliCommand(raw) {
|
|
350
386
|
return raw.replace(/_/g, "-");
|
|
351
387
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
388
|
+
const booleanFlagValues = new Set(["true", "false", "1", "0"]);
|
|
389
|
+
const isEnabledFlag = (argv, flag) => {
|
|
390
|
+
for (let index = 0; index < argv.length; index++) {
|
|
391
|
+
const value = argv[index];
|
|
392
|
+
if (value === `${flag}=true` || value === `${flag}=1`)
|
|
393
|
+
return true;
|
|
394
|
+
if (value?.startsWith(`${flag}=`))
|
|
395
|
+
return false;
|
|
396
|
+
if (value !== flag)
|
|
397
|
+
continue;
|
|
398
|
+
const next = argv[index + 1];
|
|
399
|
+
if (next !== undefined && booleanFlagValues.has(next)) {
|
|
400
|
+
return next === "true" || next === "1";
|
|
401
|
+
}
|
|
402
|
+
return true;
|
|
359
403
|
}
|
|
360
|
-
|
|
404
|
+
return false;
|
|
405
|
+
};
|
|
406
|
+
const runtimeOnlyFlags = [
|
|
407
|
+
"--allow-state-change",
|
|
408
|
+
"--allow-email",
|
|
409
|
+
"--allow-external-write",
|
|
410
|
+
"--allow-delete",
|
|
411
|
+
"--allow-financial-write",
|
|
412
|
+
"--debug",
|
|
413
|
+
"--quiet",
|
|
414
|
+
"--api-url",
|
|
415
|
+
];
|
|
416
|
+
const isRuntimeOnlyArgument = (value) => runtimeOnlyFlags.some((flag) => value === flag || value.startsWith(`${flag}=`)) || value === "-q";
|
|
417
|
+
const parseRootArguments = (argv) => {
|
|
418
|
+
const commandArgv = [];
|
|
419
|
+
let apiBaseUrl;
|
|
420
|
+
for (let index = 0; index < argv.length; index++) {
|
|
421
|
+
const value = argv[index];
|
|
422
|
+
if (value === undefined)
|
|
423
|
+
continue;
|
|
424
|
+
if (value.startsWith("--api-url=")) {
|
|
425
|
+
apiBaseUrl = value.slice("--api-url=".length);
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (value === "--api-url") {
|
|
429
|
+
const next = argv[index + 1];
|
|
430
|
+
if (next === undefined || next.startsWith("-")) {
|
|
431
|
+
throw new CliRuntimeError("USAGE", "--api-url requires a URL.");
|
|
432
|
+
}
|
|
433
|
+
apiBaseUrl = next;
|
|
434
|
+
index++;
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (isRuntimeOnlyArgument(value)) {
|
|
438
|
+
const next = argv[index + 1];
|
|
439
|
+
if (next !== undefined && booleanFlagValues.has(next))
|
|
440
|
+
index++;
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
commandArgv.push(value);
|
|
444
|
+
}
|
|
445
|
+
return { apiBaseUrl, commandArgv };
|
|
446
|
+
};
|
|
447
|
+
const effectPermissionsFromArgv = (argv) => ({
|
|
448
|
+
allowStateChange: isEnabledFlag(argv, "--allow-state-change"),
|
|
449
|
+
allowEmail: isEnabledFlag(argv, "--allow-email"),
|
|
450
|
+
allowExternalWrite: isEnabledFlag(argv, "--allow-external-write"),
|
|
451
|
+
allowDelete: isEnabledFlag(argv, "--allow-delete"),
|
|
452
|
+
allowFinancialWrite: isEnabledFlag(argv, "--allow-financial-write"),
|
|
453
|
+
});
|
|
454
|
+
const isCompletionShell = (value) => value === "bash" ||
|
|
455
|
+
value === "zsh" ||
|
|
456
|
+
value === "fish" ||
|
|
457
|
+
value === "powershell";
|
|
458
|
+
const legacyArgvForCommand = (argv, resolvedCommand) => {
|
|
459
|
+
const legacyTarget = resolvedCommand.command.legacyTarget;
|
|
460
|
+
if (legacyTarget === undefined) {
|
|
461
|
+
throw new CliRuntimeError("INTERNAL", `Command ${resolvedCommand.command.path.join(" ")} has no dispatcher target.`);
|
|
462
|
+
}
|
|
463
|
+
return [
|
|
464
|
+
argv[0] ?? "node",
|
|
465
|
+
argv[1] ?? "bb",
|
|
466
|
+
legacyTarget,
|
|
467
|
+
...resolvedCommand.argv.filter((value) => !isRuntimeOnlyArgument(value)),
|
|
468
|
+
];
|
|
469
|
+
};
|
|
470
|
+
const normalizeDispatcherError = (error) => {
|
|
471
|
+
if (!(error instanceof Error))
|
|
472
|
+
return error;
|
|
473
|
+
if (/\brequires\b|\bmust be\b|\bmust include\b|^Invalid\b|^Missing\b|^--/.test(error.message)) {
|
|
474
|
+
return new CliRuntimeError("VALIDATION", error.message);
|
|
475
|
+
}
|
|
476
|
+
return error;
|
|
477
|
+
};
|
|
478
|
+
const commandConfirmationTarget = (resolvedCommand) => {
|
|
479
|
+
const positionalTarget = resolvedCommand.argv.find((value) => value !== "--" && !value.startsWith("-"));
|
|
480
|
+
if (positionalTarget !== undefined &&
|
|
481
|
+
!positionalTarget.startsWith("{") &&
|
|
482
|
+
!positionalTarget.startsWith("[")) {
|
|
483
|
+
return positionalTarget;
|
|
484
|
+
}
|
|
485
|
+
return resolvedCommand.command.path.join(" ");
|
|
486
|
+
};
|
|
487
|
+
export async function runCli(argv = process.argv, runtime = createProcessRuntime()) {
|
|
488
|
+
const rawArgv = argv.slice(2);
|
|
489
|
+
const metadata = getCliPackageMetadata();
|
|
490
|
+
const quiet = isEnabledFlag(rawArgv, "--quiet") ||
|
|
491
|
+
rawArgv.includes("-q") ||
|
|
492
|
+
runtime.env.BB_CLI_QUIET === "1";
|
|
493
|
+
const debug = isEnabledFlag(rawArgv, "--debug");
|
|
494
|
+
let rootArguments;
|
|
495
|
+
try {
|
|
496
|
+
rootArguments = parseRootArguments(rawArgv);
|
|
497
|
+
const apiBaseUrl = rootArguments.apiBaseUrl ?? runtime.env.BB_API_URL?.trim();
|
|
498
|
+
configureBudgetBuilderApiBaseUrl(apiBaseUrl || DEFAULT_BUDGET_BUILDER_API_BASE_URL);
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
setActiveCliSession({
|
|
502
|
+
runtime,
|
|
503
|
+
invocation: {
|
|
504
|
+
command: "startup",
|
|
505
|
+
cliVersion: metadata.version,
|
|
506
|
+
apiBaseUrl: BUDGET_BUILDER_API_BASE_URL,
|
|
507
|
+
quiet,
|
|
508
|
+
debug,
|
|
509
|
+
},
|
|
510
|
+
effects: [],
|
|
511
|
+
permissions: {},
|
|
512
|
+
});
|
|
513
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
514
|
+
const exitCode = emitCommandError(error instanceof CliRuntimeError
|
|
515
|
+
? error
|
|
516
|
+
: new CliRuntimeError("USAGE", `Invalid API URL: ${message}`));
|
|
517
|
+
clearActiveCliSession();
|
|
518
|
+
return exitCode;
|
|
519
|
+
}
|
|
520
|
+
const { commandArgv } = rootArguments;
|
|
521
|
+
if (commandArgv.length === 0 ||
|
|
522
|
+
commandArgv[0] === "help" ||
|
|
523
|
+
isEnabledFlag(commandArgv, "--help") ||
|
|
524
|
+
commandArgv.includes("-h")) {
|
|
525
|
+
if (commandArgv.includes("--legacy")) {
|
|
526
|
+
printLegacyHelp(runtime);
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
runtime.stdout.write(`${createHumanHelp()}\n`);
|
|
530
|
+
}
|
|
531
|
+
return 0;
|
|
532
|
+
}
|
|
533
|
+
if (commandArgv[0] === "completion") {
|
|
534
|
+
const shell = commandArgv[1];
|
|
535
|
+
if (shell === undefined || !isCompletionShell(shell)) {
|
|
536
|
+
setActiveCliSession({
|
|
537
|
+
runtime,
|
|
538
|
+
invocation: {
|
|
539
|
+
command: "completion",
|
|
540
|
+
cliVersion: metadata.version,
|
|
541
|
+
apiBaseUrl: BUDGET_BUILDER_API_BASE_URL,
|
|
542
|
+
quiet,
|
|
543
|
+
debug,
|
|
544
|
+
},
|
|
545
|
+
effects: [],
|
|
546
|
+
permissions: {},
|
|
547
|
+
});
|
|
548
|
+
const exitCode = emitCommandError(new CliRuntimeError("USAGE", "completion requires bash, zsh, fish, or powershell."));
|
|
549
|
+
clearActiveCliSession();
|
|
550
|
+
return exitCode;
|
|
551
|
+
}
|
|
552
|
+
runtime.stdout.write(createCompletionScript(shell));
|
|
553
|
+
return 0;
|
|
554
|
+
}
|
|
555
|
+
if (commandArgv[0] === "version" ||
|
|
556
|
+
isEnabledFlag(commandArgv, "--version") ||
|
|
557
|
+
commandArgv.includes("-v")) {
|
|
558
|
+
setActiveCliSession({
|
|
559
|
+
runtime,
|
|
560
|
+
invocation: {
|
|
561
|
+
command: "version",
|
|
562
|
+
cliVersion: metadata.version,
|
|
563
|
+
apiBaseUrl: BUDGET_BUILDER_API_BASE_URL,
|
|
564
|
+
quiet,
|
|
565
|
+
debug,
|
|
566
|
+
},
|
|
567
|
+
effects: [],
|
|
568
|
+
permissions: {},
|
|
569
|
+
});
|
|
570
|
+
emitCommandResult(getVersionData());
|
|
571
|
+
clearActiveCliSession();
|
|
572
|
+
return 0;
|
|
573
|
+
}
|
|
574
|
+
const resolvedCommand = resolveCommand(commandArgv);
|
|
575
|
+
if (resolvedCommand === undefined) {
|
|
576
|
+
setActiveCliSession({
|
|
577
|
+
runtime,
|
|
578
|
+
invocation: {
|
|
579
|
+
command: commandArgv[0] ?? "unknown",
|
|
580
|
+
cliVersion: metadata.version,
|
|
581
|
+
apiBaseUrl: BUDGET_BUILDER_API_BASE_URL,
|
|
582
|
+
quiet,
|
|
583
|
+
debug,
|
|
584
|
+
},
|
|
585
|
+
effects: [],
|
|
586
|
+
permissions: {},
|
|
587
|
+
});
|
|
588
|
+
const exitCode = emitCommandError(new CliRuntimeError("USAGE", `Unknown command: ${commandArgv.join(" ")}. Run 'bb help'.`));
|
|
589
|
+
clearActiveCliSession();
|
|
590
|
+
return exitCode;
|
|
591
|
+
}
|
|
592
|
+
const effectiveArgv = legacyArgvForCommand(argv, resolvedCommand);
|
|
593
|
+
const { command, positional, flags } = parseArgs(effectiveArgv);
|
|
594
|
+
consumeCliQuietFlags(flags);
|
|
595
|
+
setCliQuiet(resolvedCommand.isLegacyAlias ? quiet : quiet || !debug);
|
|
596
|
+
setActiveCliSession({
|
|
597
|
+
runtime,
|
|
598
|
+
invocation: {
|
|
599
|
+
command: resolvedCommand.command.path.join(" "),
|
|
600
|
+
cliVersion: metadata.version,
|
|
601
|
+
apiBaseUrl: BUDGET_BUILDER_API_BASE_URL,
|
|
602
|
+
legacy: resolvedCommand.isLegacyAlias,
|
|
603
|
+
quiet,
|
|
604
|
+
debug,
|
|
605
|
+
},
|
|
606
|
+
effects: resolvedCommand.command.effects,
|
|
607
|
+
permissions: effectPermissionsFromArgv(rawArgv),
|
|
608
|
+
});
|
|
609
|
+
if (resolvedCommand.isLegacyAlias && !quiet) {
|
|
610
|
+
runtime.stderr.write(`[bb] Deprecated command "${resolvedCommand.invokedAs.join(" ")}"; use "bb ${resolvedCommand.command.path.join(" ")}".\n`);
|
|
611
|
+
}
|
|
612
|
+
const cmd = normalizeCliCommand(command ?? "");
|
|
361
613
|
const t0 = Date.now();
|
|
362
614
|
if (shouldLogCliActions()) {
|
|
363
615
|
logCliAction({
|
|
@@ -369,7 +621,20 @@ async function main() {
|
|
|
369
621
|
});
|
|
370
622
|
}
|
|
371
623
|
try {
|
|
624
|
+
requireApiKey();
|
|
625
|
+
if (!resolvedCommand.isLegacyAlias &&
|
|
626
|
+
resolvedCommand.command.effects.length > 0) {
|
|
627
|
+
await confirmCurrentCommand({
|
|
628
|
+
action: resolvedCommand.command.path.join(" "),
|
|
629
|
+
target: commandConfirmationTarget(resolvedCommand),
|
|
630
|
+
details: "Canonical v2 mutation. Supply every effect-specific --allow-* flag for non-interactive execution.",
|
|
631
|
+
});
|
|
632
|
+
}
|
|
372
633
|
switch (cmd) {
|
|
634
|
+
case "whoami": {
|
|
635
|
+
await whoAmI();
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
373
638
|
case "list-budgets": {
|
|
374
639
|
const statusStr = getFlag(flags, "status");
|
|
375
640
|
const statuses = parseCommaSeparatedBudgetStatuses(statusStr !== undefined ? String(statusStr) : undefined);
|
|
@@ -415,6 +680,9 @@ async function main() {
|
|
|
415
680
|
? String(getFlag(flags, "projectManagerId"))
|
|
416
681
|
: undefined,
|
|
417
682
|
markProjectWon: parseOptionalBoolFlag(flags, "markProjectWon"),
|
|
683
|
+
projectInvoiceSettings: getFlag(flags, "invoiceSettings") !== undefined
|
|
684
|
+
? parseJsonFlag(String(getFlag(flags, "invoiceSettings")), "--invoiceSettings")
|
|
685
|
+
: undefined,
|
|
418
686
|
});
|
|
419
687
|
break;
|
|
420
688
|
}
|
|
@@ -474,6 +742,9 @@ async function main() {
|
|
|
474
742
|
: undefined,
|
|
475
743
|
markProjectWon: parseOptionalBoolFlag(flags, "markProjectWon"),
|
|
476
744
|
projectStatusOnCommercialRejection: parseCommercialRejectionProjectStatus(flags),
|
|
745
|
+
projectInvoiceSettings: getFlag(flags, "invoiceSettings") !== undefined
|
|
746
|
+
? parseJsonFlag(String(getFlag(flags, "invoiceSettings")), "--invoiceSettings")
|
|
747
|
+
: undefined,
|
|
477
748
|
});
|
|
478
749
|
break;
|
|
479
750
|
}
|
|
@@ -1070,19 +1341,39 @@ async function main() {
|
|
|
1070
1341
|
await checkCustomerInvoiceReadiness(budgetId);
|
|
1071
1342
|
break;
|
|
1072
1343
|
}
|
|
1073
|
-
case "list-customer-
|
|
1074
|
-
const
|
|
1075
|
-
if (!
|
|
1076
|
-
throw new Error("list-customer-
|
|
1344
|
+
case "list-eligible-customer-invoice-budgets": {
|
|
1345
|
+
const projectId = positional[0];
|
|
1346
|
+
if (!projectId) {
|
|
1347
|
+
throw new Error("list-eligible-customer-invoice-budgets requires <projectId>");
|
|
1077
1348
|
}
|
|
1349
|
+
await listEligibleCustomerInvoiceBudgets(projectId);
|
|
1350
|
+
break;
|
|
1351
|
+
}
|
|
1352
|
+
case "list-customer-invoices": {
|
|
1353
|
+
const positionalBudgetId = positional[0];
|
|
1354
|
+
const projectId = getFlag(flags, "projectId");
|
|
1355
|
+
const projectIds = parseCommaSeparatedIds(getFlag(flags, "projectIds"));
|
|
1356
|
+
if (projectId && projectIds) {
|
|
1357
|
+
throw new Error("Use either --projectId for project scope or --projectIds for a global filter, not both.");
|
|
1358
|
+
}
|
|
1359
|
+
const budgetIds = [
|
|
1360
|
+
...(positionalBudgetId ? [positionalBudgetId] : []),
|
|
1361
|
+
...(parseCommaSeparatedIds(getFlag(flags, "budgetIds")) ?? []),
|
|
1362
|
+
];
|
|
1078
1363
|
const statusRaw = getFlag(flags, "status");
|
|
1079
1364
|
const sortDir = parseOptionalBillListSortDir(getFlag(flags, "sortDir"));
|
|
1080
1365
|
await listCustomerInvoices({
|
|
1081
|
-
|
|
1366
|
+
scope: projectId
|
|
1367
|
+
? { type: "project", projectId }
|
|
1368
|
+
: { type: "global" },
|
|
1082
1369
|
page: parsePositiveIntFlag(getFlag(flags, "page"), "--page") ?? 1,
|
|
1083
|
-
perPage: parsePositiveIntFlag(getFlag(flags, "perPage"), "--perPage") ??
|
|
1370
|
+
perPage: parsePositiveIntFlag(getFlag(flags, "perPage"), "--perPage") ?? 20,
|
|
1084
1371
|
q: getFlag(flags, "search") ?? "",
|
|
1085
1372
|
statuses: parseCommaSeparatedCustomerInvoiceStatuses(statusRaw !== undefined ? String(statusRaw) : undefined),
|
|
1373
|
+
companyIds: parseCommaSeparatedIds(getFlag(flags, "companyIds")),
|
|
1374
|
+
projectIds,
|
|
1375
|
+
budgetIds: budgetIds.length > 0 ? [...new Set(budgetIds)] : undefined,
|
|
1376
|
+
createdByIds: parseCommaSeparatedIds(getFlag(flags, "createdByIds")),
|
|
1086
1377
|
sortBy: parseCustomerInvoiceSortBy(getFlag(flags, "sortBy")),
|
|
1087
1378
|
sortDir: sortDir ?? "desc",
|
|
1088
1379
|
});
|
|
@@ -1096,6 +1387,14 @@ async function main() {
|
|
|
1096
1387
|
await getCustomerInvoice(batchId);
|
|
1097
1388
|
break;
|
|
1098
1389
|
}
|
|
1390
|
+
case "get-customer-invoice-email-context": {
|
|
1391
|
+
const batchId = positional[0];
|
|
1392
|
+
if (!batchId) {
|
|
1393
|
+
throw new Error("get-customer-invoice-email-context requires <batchId>");
|
|
1394
|
+
}
|
|
1395
|
+
await getCustomerInvoiceEmailContext(batchId);
|
|
1396
|
+
break;
|
|
1397
|
+
}
|
|
1099
1398
|
case "create-customer-invoice": {
|
|
1100
1399
|
const payloadRaw = getFlag(flags, "payload");
|
|
1101
1400
|
if (!payloadRaw) {
|
|
@@ -1145,6 +1444,37 @@ async function main() {
|
|
|
1145
1444
|
await rejectCustomerInvoice(batchId, String(reason));
|
|
1146
1445
|
break;
|
|
1147
1446
|
}
|
|
1447
|
+
case "send-customer-invoice-to-contact-person": {
|
|
1448
|
+
const payloadRaw = getFlag(flags, "payload");
|
|
1449
|
+
if (!payloadRaw) {
|
|
1450
|
+
throw new Error("send-customer-invoice-to-contact-person requires --payload '<json>'");
|
|
1451
|
+
}
|
|
1452
|
+
await sendCustomerInvoiceToContactPersonFromPayload(parseJsonFlag(String(payloadRaw), "--payload"));
|
|
1453
|
+
break;
|
|
1454
|
+
}
|
|
1455
|
+
case "mark-customer-invoice-paid": {
|
|
1456
|
+
const invoiceId = positional[0];
|
|
1457
|
+
const paymentDate = getFlag(flags, "paymentDate");
|
|
1458
|
+
const paymentProofPath = getFlag(flags, "paymentProof");
|
|
1459
|
+
if (!invoiceId || !paymentDate || !paymentProofPath) {
|
|
1460
|
+
throw new Error("mark-customer-invoice-paid requires <invoiceId> --paymentDate YYYY-MM-DD --paymentProof <path> [--paymentReference <text>]");
|
|
1461
|
+
}
|
|
1462
|
+
await markCustomerInvoicePaidFromPath({
|
|
1463
|
+
invoiceId,
|
|
1464
|
+
paymentDate,
|
|
1465
|
+
paymentProofPath,
|
|
1466
|
+
paymentReference: getFlag(flags, "paymentReference"),
|
|
1467
|
+
});
|
|
1468
|
+
break;
|
|
1469
|
+
}
|
|
1470
|
+
case "download-customer-invoice-payment-proof": {
|
|
1471
|
+
const invoiceId = positional[0];
|
|
1472
|
+
if (!invoiceId) {
|
|
1473
|
+
throw new Error("download-customer-invoice-payment-proof requires <invoiceId> [--output <path>]");
|
|
1474
|
+
}
|
|
1475
|
+
await downloadCustomerInvoicePaymentProof(invoiceId, getFlag(flags, "output"));
|
|
1476
|
+
break;
|
|
1477
|
+
}
|
|
1148
1478
|
case "download-customer-invoice-pdf": {
|
|
1149
1479
|
const invoiceId = positional[0];
|
|
1150
1480
|
if (!invoiceId) {
|
|
@@ -1601,8 +1931,8 @@ async function main() {
|
|
|
1601
1931
|
case "create-api-key": {
|
|
1602
1932
|
const userId = getFlag(flags, "userId");
|
|
1603
1933
|
const name = getFlag(flags, "name");
|
|
1604
|
-
if (!
|
|
1605
|
-
throw new Error("create-api-key requires --
|
|
1934
|
+
if (!name) {
|
|
1935
|
+
throw new Error("create-api-key requires --name <label> [--userId <userId>]");
|
|
1606
1936
|
}
|
|
1607
1937
|
await createApiKeyForUser({ userId, name });
|
|
1608
1938
|
break;
|
|
@@ -1737,6 +2067,8 @@ async function main() {
|
|
|
1737
2067
|
durationMs: Date.now() - t0,
|
|
1738
2068
|
});
|
|
1739
2069
|
}
|
|
2070
|
+
clearActiveCliSession();
|
|
2071
|
+
return 0;
|
|
1740
2072
|
}
|
|
1741
2073
|
catch (err) {
|
|
1742
2074
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1749,8 +2081,21 @@ async function main() {
|
|
|
1749
2081
|
error: message,
|
|
1750
2082
|
});
|
|
1751
2083
|
}
|
|
1752
|
-
|
|
1753
|
-
|
|
2084
|
+
const exitCode = emitCommandError(normalizeDispatcherError(err));
|
|
2085
|
+
clearActiveCliSession();
|
|
2086
|
+
return exitCode;
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
const resolveExecutablePath = (value) => {
|
|
2090
|
+
try {
|
|
2091
|
+
return realpathSync(value);
|
|
2092
|
+
}
|
|
2093
|
+
catch {
|
|
2094
|
+
return resolve(value);
|
|
1754
2095
|
}
|
|
2096
|
+
};
|
|
2097
|
+
const isDirectExecution = process.argv[1] !== undefined &&
|
|
2098
|
+
resolveExecutablePath(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
2099
|
+
if (isDirectExecution) {
|
|
2100
|
+
process.exitCode = await runCli();
|
|
1755
2101
|
}
|
|
1756
|
-
main();
|
package/dist/load-env.js
CHANGED
|
@@ -42,24 +42,37 @@ const requiredEmail = (value, fieldLabel) => {
|
|
|
42
42
|
}
|
|
43
43
|
return email;
|
|
44
44
|
};
|
|
45
|
+
const parseContactPersonEmailPayload = (input, commandName) => {
|
|
46
|
+
if (!Array.isArray(input.cc)) {
|
|
47
|
+
throw new Error(`${commandName} payload.cc must be an email array.`);
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
to: requiredEmail(input.to, `${commandName} payload.to`),
|
|
51
|
+
cc: input.cc.map((email, index) => requiredEmail(email, `${commandName} payload.cc[${index}]`)),
|
|
52
|
+
replyTo: requiredEmail(input.replyTo, `${commandName} payload.replyTo`),
|
|
53
|
+
subject: requiredString(input.subject, `${commandName} payload.subject`),
|
|
54
|
+
content: requiredString(input.content, `${commandName} payload.content`),
|
|
55
|
+
signature: requiredString(input.signature, `${commandName} payload.signature`),
|
|
56
|
+
};
|
|
57
|
+
};
|
|
45
58
|
/** Matches the web estimate composer (`email.sendEstimateToContactPerson`). */
|
|
46
59
|
export const parseSendEstimateToContactPersonPayload = (raw) => {
|
|
47
60
|
const input = requireObject(raw, "send-estimate-to-contact-person payload");
|
|
48
|
-
if (!Array.isArray(input.cc)) {
|
|
49
|
-
throw new Error("send-estimate-to-contact-person payload.cc must be an email array.");
|
|
50
|
-
}
|
|
51
61
|
return {
|
|
52
|
-
|
|
53
|
-
cc: input.cc.map((email, index) => requiredEmail(email, `send-estimate-to-contact-person payload.cc[${index}]`)),
|
|
54
|
-
replyTo: requiredEmail(input.replyTo, "send-estimate-to-contact-person payload.replyTo"),
|
|
55
|
-
subject: requiredString(input.subject, "send-estimate-to-contact-person payload.subject"),
|
|
56
|
-
content: requiredString(input.content, "send-estimate-to-contact-person payload.content"),
|
|
57
|
-
signature: requiredString(input.signature, "send-estimate-to-contact-person payload.signature"),
|
|
62
|
+
...parseContactPersonEmailPayload(input, "send-estimate-to-contact-person"),
|
|
58
63
|
estimateId: requiredString(input.estimateId, "send-estimate-to-contact-person payload.estimateId"),
|
|
59
64
|
estimateDocNumber: optionalString(input.estimateDocNumber),
|
|
60
65
|
budgetId: requiredString(input.budgetId, "send-estimate-to-contact-person payload.budgetId"),
|
|
61
66
|
};
|
|
62
67
|
};
|
|
68
|
+
/** Matches the web customer-invoice email composer. */
|
|
69
|
+
export const parseSendCustomerInvoiceToContactPersonPayload = (raw) => {
|
|
70
|
+
const input = requireObject(raw, "send-customer-invoice-to-contact-person payload");
|
|
71
|
+
return {
|
|
72
|
+
...parseContactPersonEmailPayload(input, "send-customer-invoice-to-contact-person"),
|
|
73
|
+
invoiceId: requiredString(input.invoiceId, "send-customer-invoice-to-contact-person payload.invoiceId"),
|
|
74
|
+
};
|
|
75
|
+
};
|
|
63
76
|
/** Matches budget.createBudget — optional pipedriveDealId; Asana deal is project.asanaTaskId. */
|
|
64
77
|
export function parseCreateBudgetPayload(raw) {
|
|
65
78
|
const o = requireObject(raw, "create-budget payload");
|