@go-labs-sg/bb 1.17.0 → 1.19.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/dist/index.js CHANGED
@@ -2,9 +2,9 @@
2
2
  import "./load-env.js";
3
3
  import { requireApiKey } from "./api-client.js";
4
4
  import { consumeCliQuietFlags, logCliAction, sanitizeFlagsForTrace, setCliQuiet, shouldLogCliActions, } from "./cli-trace.js";
5
- import { addBudgetItems, approveBill, approveBudget, approveQuotation, approveSupplier, createApiKeyForUser, createBillApproval, createBillFromPayload, createBudgetApproval, createBudgetCategory, createBudgetFromPayload, createCompany, createContactPersonFromPayload, createEstimate, createItemCategory, createItemFromPayload, createProject, createQuotationFromPayload, createSupplierCertification, createSupplierFromPayload, createSupplierPaymentMethod, createSupplierRoleOption, createSupplierTagOption, createUser, deleteBillById, deleteBudgetById, deleteBudgetCategory, deleteCompanyById, deleteItemById, deleteItemCategoriesByIds, deleteProjectById, deleteSuppliersByIds, downloadQuotationPdf, getApprovedBudgets, getBillAttachments, getBillDetails, getBudget, getBudgetCategories, getBudgetCategoryBenchmarks, getBudgetDetails, getBudgetItemsOnly, getBudgetVersions, getCompany, getDashboard, getErrorMetrics, getEstimatePerformance, getFinancialOverview, getItem, getItemPricingHistory, getMonthlyMetrics, getProject, getQuotationDetails, getRecentErrors, getSupplierAnalytics, getSupplierDetails, getSupplierPricingHistory, getSystemOverview, getUserPerformance, listApiKeysForUser, listApprovals, listBills, listBudgets, listCompanies, listContacts, listItemCategories, listItems, listProjects, listQuotations, listSuppliers, listUsers, markBudgetWonWithProof, patchBillInvoiceNumber, patchBillPayment, reactivateSuppliersByIds, rejectBill, rejectBudget, rejectQuotation, rejectSupplier, removeBudgetItem, reorderBudgetItemsCli, revokeApiKeyForUser, submitQuotation, updateBillFromPayload, updateBillStatus, updateBudgetCategory, updateBudgetCommissionFromPayload, updateBudgetDiscountFromPayload, updateBudgetFromPayload, updateBudgetItem, updateBudgetItemSupplierCli, updateBudgetStatus, updateCompanyFromPayload, updateContactPersonFromPayload, updateItemCategory, updateItemFromPayload, updateProjectFromPayload, updateProjectStatus, updateSupplierFromPayload, uploadBillAttachmentFromPath, uploadBillAttachmentsFromPaths, uploadBillDocumentsFromPaths, uploadBudgetAttachmentFromPath, uploadQuotationAttachmentFromPath, } from "./commands.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, 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";
6
6
  import { getFlag, parseArgs } from "./parse-args.js";
7
- import { billStatusesForUpdateHelp, budgetStatusesForHelp, isBudgetStatusUpdate, parseApprovalTypeFlag, parseBillStatusForUpdate, parseCommaSeparatedBillStatuses, parseCommaSeparatedBudgetStatuses, parseCommaSeparatedIds, parseCommaSeparatedSupplierStatuses, parseOptionalBillListSortBy, parseOptionalBillListSortDir, parseOptionalDashboardRole, parseOptionalDeals, parseOptionalErrorSeverity, parseOptionalErrorStatus, parseOptionalExtendedProjectStatus, parseOptionalFinancialRole, parseOptionalSupplierAnalyticsTimeFrame, parseOptionalTimeFrame, parseProjectStatusForUpdate, parseUserRole, projectStatusesForHelp, userRolesForHelp, } from "./parse-cli-enums.js";
7
+ 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
8
  import { parseJsonFlag, parseOptionalNumber as parseOptNum, } from "./parse-json-flag.js";
9
9
  function parsePositiveIntFlag(value, flagName) {
10
10
  if (value === undefined)
@@ -26,6 +26,35 @@ function parseOptionalBoolFlag(flags, key) {
26
26
  return false;
27
27
  throw new Error(`--${key} must be true or false.`);
28
28
  }
29
+ const CUSTOMER_INVOICE_SORT_FIELDS = [
30
+ "createdAt",
31
+ "status",
32
+ "totalNetAmount",
33
+ "approvalExpiresAt",
34
+ ];
35
+ function parseCustomerInvoiceSortBy(value) {
36
+ if (value === undefined)
37
+ return "createdAt";
38
+ if (!CUSTOMER_INVOICE_SORT_FIELDS.includes(value)) {
39
+ throw new Error(`Invalid --sortBy "${value}". Use one of: ${CUSTOMER_INVOICE_SORT_FIELDS.join(", ")}.`);
40
+ }
41
+ return value;
42
+ }
43
+ const INTEGRATION_OPERATION_STATUSES = [
44
+ "PENDING",
45
+ "PROCESSING",
46
+ "FAILED",
47
+ "COMPLETED",
48
+ ];
49
+ function parseIntegrationOperationStatus(value) {
50
+ if (value === undefined)
51
+ return undefined;
52
+ const normalized = value.toUpperCase();
53
+ if (!INTEGRATION_OPERATION_STATUSES.includes(normalized)) {
54
+ throw new Error(`Invalid --status "${value}". Use one of: ${INTEGRATION_OPERATION_STATUSES.join(", ")}.`);
55
+ }
56
+ return normalized;
57
+ }
29
58
  function parseCommercialRejectionProjectStatus(flags) {
30
59
  const raw = flags.projectStatusOnCommercialRejection;
31
60
  if (raw === undefined)
@@ -120,8 +149,9 @@ Global: --quiet | -q | BB_CLI_QUIET=1 — hide action logs (default: log command
120
149
  Auth: Set BB_API_KEY in the environment (same API key as MCP / Goracle).
121
150
 
122
151
  Sensitive workflow changes: approval decisions/requests, entity status updates,
123
- mark-budget-won, submit-quotation, and supplier approval request side effects
124
- require an interactive CONFIRM prompt and abort in non-interactive shells.
152
+ customer invoices, integration retries, mark-budget-won, submit-quotation, and
153
+ supplier approval request side effects require an interactive CONFIRM prompt
154
+ and abort in non-interactive shells.
125
155
  AI agents must inspect the entity first, summarize the exact entity, target
126
156
  state, and side effects, then wait for explicit user confirmation before running
127
157
  the sensitive command.
@@ -133,6 +163,8 @@ Budgets
133
163
  get-budget-details <id> Full detail (margins, suppliers, etc.)
134
164
  get-budget-categories
135
165
  get-budget-versions <budgetId>
166
+ rename-budget-version <versionId> --name <text>
167
+ restore-budget-version <versionId> Replace current budget data and return it to DRAFT.
136
168
  update-budget-status <id> <status> [--projectManagerId <id>] [--markProjectWon true|false] [--projectStatusOnCommercialRejection PITCH|LOST]
137
169
  Budget-changing commands require interactive "yes" confirmation when the current budget is ESTIMATE_CREATED, ESTIMATE_SENT, ESTIMATE_ACCEPTED, ESTIMATE_REJECTED, or ESTIMATE_CLOSED.
138
170
  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.
@@ -145,7 +177,7 @@ Budgets
145
177
  create-budget-approval <budgetId> (also sends approval request emails)
146
178
  create-estimate <budgetId> (creates QuickBooks estimate; supports preflight updates)
147
179
  add-budget-items --budgetId --items '[{"itemId":"…","quantity":1,"markup":30},…]'
148
- update-budget-item --id <budgetItemId> [--description plain-text] [--note plain-text] [--quantity] [--markup] [--cost] [--unitPrice] [--isFreeOfCharge true|false] [--gstInclusive true|false]
180
+ update-budget-item --id <budgetItemId> [--description plain-text] [--note plain-text] [--quantity] [--markup] [--cost] [--unitPrice] [--isFreeOfCharge true|false] [--gstInclusive true|false] [--gstOutOfScope true|false]
149
181
  remove-budget-item <budgetItemId>
150
182
  reorder-budget-items <budgetId> --itemIds <csv>
151
183
  update-budget-item-supplier --budgetItemId --supplierId
@@ -154,6 +186,11 @@ Budgets
154
186
  delete-budget-category <id>
155
187
  update-budget-commission --payload '<json>' (budget.updateBudgetCommission)
156
188
  update-budget-discount --payload '<json>' (budget.updateBudgetDiscount; type percentage|amount)
189
+ delete-budget-commission <budgetId> Remove commission and return the budget to DRAFT.
190
+ delete-budget-discount <budgetId> Remove discount and return the budget to DRAFT.
191
+ mark-budget-items-not-utilized --ids <csv> Remove unused lines from reconciliation and clear QBO placeholders.
192
+ restore-budget-item <budgetItemId> Restore one Not Utilized line and its QBO placeholder when required.
193
+ create-placeholder-bill <budgetItemId> Admin-only placeholder recovery.
157
194
  upload-budget-attachment <budgetId> <filePath> Presigned S3 upload + confirm
158
195
 
159
196
  Bills
@@ -162,11 +199,16 @@ Bills
162
199
  isClaimable differentiates the shared bill/claim records: false = bill, true = claim.
163
200
  New supplier bills from 1 Jul 2026 00:00 SGT require approved quotation coverage for each non-legacy line item.
164
201
  create-bill-approval <billId> (also sends approval request emails)
165
- create-bill --payload '<json>' (bill.create; payload.isClaimable false = bill, true = claim; use upload-bill-attachment after create to add files)
202
+ create-bill --payload '<json>' (bill.create; payload.isClaimable false = bill, true = claim)
203
+ validate-bill-selection --payload '<json>' Check projectId, supplierId, budgetItemIds, and optional alreadyPaid before creation.
204
+ stage-bill-attachment <projectId> <filePath...> [--file <path>] [--files <csv>] Upload files before create-bill; returns attachment JSON for attachments/paymentProofAttachments
205
+ cleanup-staged-bill-attachments <projectId> --keys <csv> Delete unattached staged bill/claim uploads.
166
206
  update-bill --payload '<json>' (bill.update; must include id)
207
+ update-bill-payment-evidence --payload '<json>' Replace paymentReference/paymentProofAttachments for an already-paid bill.
167
208
  delete-bill <billId>
168
209
  update-bill-status <id> <status> [--rejectionReason] [--paymentTrackingUrl] [--paymentReference]
169
210
  status (${billStatusesForUpdateHelp.join(", ")}) — not PENDING_APPROVAL; use create-bill-approval
211
+ Moving a bill or claim to PAID sends creator and supplier emails, CC m@getout.sg and BCC noah@getout.sg; duplicate claim recipients receive one email.
170
212
  patch-bill-payment <billId> [--paymentTrackingUrl] [--paymentReference] [--quickbooksBillId] [--paymentDate <ISO>]
171
213
  Clear a field: --clearPaymentTrackingUrl true | --clearPaymentReference true | --clearQuickbooksBillId true | --clearPaymentDate true
172
214
  patch-bill-invoice-number <billId> <invoiceNumber>
@@ -177,17 +219,33 @@ Bills
177
219
  get-bill-details <billId>
178
220
 
179
221
  Quotations
180
- list-quotations [--projectId] [--budgetId] [--supplierId] [--status CSV] [--page] [--pageSize]
222
+ list-quotations [--projectId] [--budgetId] [--supplierId] [--status CSV] [--requesterIds CSV] [--search] [--page] [--pageSize]
181
223
  get-quotation <quotationId>
182
- upload-quotation-attachment <projectId> <filePath> Presigned S3 upload; returns attachment JSON for create-quotation payload
183
- create-quotation --payload '<json>' (quotation.createDraft)
224
+ upload-quotation-attachment <projectId> <filePath> Upload one PDF, JPEG, or PNG up to 20MB; returns attachment JSON for a quotation payload.
225
+ cleanup-staged-quotation-attachments <projectId> --keys <csv> Delete unattached staged quotation uploads.
226
+ create-quotation --payload '<json>' (quotation.createDraft; requires amountWithoutGst and gstAmount)
227
+ update-quotation --payload '<json>' Update a DRAFT or REJECTED quotation; same payload plus id.
228
+ delete-quotation <quotationId>
184
229
  submit-quotation <quotationId>
185
230
  approve-quotation <quotationId>
186
231
  reject-quotation <quotationId> --reason <text>
187
232
  download-quotation-pdf <quotationId> [original|staff|final]
188
233
 
234
+ Customer invoices
235
+ check-customer-invoice-readiness <budgetId>
236
+ list-customer-invoices <budgetId> [--status CSV] [--search] [--sortBy createdAt|status|totalNetAmount|approvalExpiresAt] [--sortDir asc|desc] [--page] [--perPage]
237
+ get-customer-invoice <batchId>
238
+ create-customer-invoice --payload '<json>' budgetId + one split with label, percentage, and dueDate.
239
+ discard-customer-invoice <batchId> Discard a reserved CREATING batch with no created QBO invoices.
240
+ delete-customer-invoice <batchId> Delete QBO invoices and the local batch.
241
+ void-customer-invoice <batchId> Void the invoice batch in QuickBooks.
242
+ approve-customer-invoice <batchId> Admin approval; notifies the creator.
243
+ reject-customer-invoice <batchId> --reason <text> Admin rejection; voids QBO invoices and notifies the creator.
244
+ download-customer-invoice-pdf <invoiceId> [--output <path>]
245
+ sync-customer-invoice <invoiceId> Refresh local status and balance from QuickBooks.
246
+
189
247
  Approvals
190
- list-approvals | get-pending-approvals [--type budget|supplier|bill|quotation|all]
248
+ list-approvals | get-pending-approvals [--type budget|supplier|bill|quotation|customer_invoice|all]
191
249
  approve-bill <billId> (also sends requester reply email)
192
250
  reject-bill <billId> --reason <text> (also sends requester reply email)
193
251
  approve-budget <budgetId> (also sends requester reply email)
@@ -205,11 +263,16 @@ Companies & projects
205
263
  list-projects [--companyId] [--name] [--status] [--active true|false] [--page] [--perPage]
206
264
  Omit --active to include both active and inactive projects.
207
265
  get-project <id>
208
- create-project --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --startDate <ISO> [--asanaTaskId] [--asanaSearch <text>] [--slackChannelId --slackChannelUrl --slackChannelName] [--pax] [--endDate <ISO>] [--description]
209
- update-project --payload '<json>' (project.updateProject; optional requestQboAccountantNotification; default notify like web)
266
+ Budget overview totalRevenue includes Estimate Accepted and Estimate Closed budgets only.
267
+ create-project --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --startDate <ISO> [--projectManagerId <userId>] [--asanaTaskId] [--asanaSearch <text>] [--slackChannelId --slackChannelUrl --slackChannelName] [--pax] [--endDate <ISO>] [--description]
268
+ update-project --payload '<json>' (project.updateProject; optional projectManagerId and requestQboAccountantNotification; default notify like web)
210
269
  delete-project <id>
211
- update-project-status <id> <status> [--projectManagerId <id>] [--projectManagerName <name>] [--wonOverrideReason <text>]
212
- Required when moving PITCH WON: supply --projectManagerId or --projectManagerName.
270
+ check-project-reconciliation <id> Run the same live reconciliation review as the web app without changing status.
271
+ reconcile-project <id> Rerun all checks transactionally and mark an eligible WON project RECONCILED.
272
+ complete-project <id> Rerun reconciliation validation, mark a RECONCILED project COMPLETED, and queue QBO placeholder cleanup.
273
+ import-qbo-project <projectId> --url <quickBooksProjectUrl>
274
+ update-project-status <id> <status> [--projectManagerId <userId>] [--projectManagerEmail <email>] [--wonOverrideReason <text>]
275
+ Required when moving PITCH → WON: supply --projectManagerId or --projectManagerEmail.
213
276
  Required when moving to WON without accepted/closed budget or win proof: --wonOverrideReason (10+ chars).
214
277
  status: ${projectStatusesForHelp.join(", ")}
215
278
 
@@ -258,6 +321,10 @@ Errors
258
321
  get-recent-errors [--page] [--perPage] [--severity] [--status]
259
322
  get-error-metrics
260
323
 
324
+ Automation (admin)
325
+ list-integration-operations [--destination] [--status PENDING|PROCESSING|FAILED|COMPLETED] [--page] [--perPage]
326
+ retry-integration-operation <operationId> [--confirmExternalStateReconciled true|false]
327
+
261
328
  Historical / benchmarks
262
329
  get-approved-budgets [--projectId] [--limit] [--minRevenue] [--maxRevenue]
263
330
  get-budget-category-benchmarks <categoryId> [--limit]
@@ -374,6 +441,23 @@ async function main() {
374
441
  await getBudgetVersions(id);
375
442
  break;
376
443
  }
444
+ case "rename-budget-version": {
445
+ const versionId = positional[0];
446
+ const newName = getFlag(flags, "name");
447
+ if (!versionId || newName === undefined) {
448
+ throw new Error("rename-budget-version requires <versionId> --name <text>");
449
+ }
450
+ await renameBudgetVersion(versionId, String(newName));
451
+ break;
452
+ }
453
+ case "restore-budget-version": {
454
+ const versionId = positional[0];
455
+ if (!versionId) {
456
+ throw new Error("restore-budget-version requires <versionId>");
457
+ }
458
+ await restoreBudgetVersion(versionId);
459
+ break;
460
+ }
377
461
  case "update-budget-status": {
378
462
  const [id, status] = positional;
379
463
  if (!id || !status) {
@@ -478,6 +562,7 @@ async function main() {
478
562
  unitPrice: parseOptNum(getFlag(flags, "unitPrice"), "--unitPrice"),
479
563
  isFreeOfCharge: parseOptionalBoolFlag(flags, "isFreeOfCharge"),
480
564
  gstInclusive: parseOptionalBoolFlag(flags, "gstInclusive"),
565
+ gstOutOfScope: parseOptionalBoolFlag(flags, "gstOutOfScope"),
481
566
  });
482
567
  break;
483
568
  }
@@ -556,6 +641,46 @@ async function main() {
556
641
  await updateBudgetDiscountFromPayload(raw);
557
642
  break;
558
643
  }
644
+ case "delete-budget-commission": {
645
+ const budgetId = positional[0];
646
+ if (!budgetId) {
647
+ throw new Error("delete-budget-commission requires <budgetId>");
648
+ }
649
+ await deleteBudgetCommission(budgetId);
650
+ break;
651
+ }
652
+ case "delete-budget-discount": {
653
+ const budgetId = positional[0];
654
+ if (!budgetId) {
655
+ throw new Error("delete-budget-discount requires <budgetId>");
656
+ }
657
+ await deleteBudgetDiscount(budgetId);
658
+ break;
659
+ }
660
+ case "mark-budget-items-not-utilized": {
661
+ const budgetItemIds = parseCommaSeparatedIds(getFlag(flags, "ids"));
662
+ if (!budgetItemIds?.length) {
663
+ throw new Error("mark-budget-items-not-utilized requires --ids <csv>");
664
+ }
665
+ await setBudgetItemsNotUtilized(budgetItemIds, true);
666
+ break;
667
+ }
668
+ case "restore-budget-item": {
669
+ const budgetItemId = positional[0];
670
+ if (!budgetItemId) {
671
+ throw new Error("restore-budget-item requires <budgetItemId>");
672
+ }
673
+ await setBudgetItemsNotUtilized([budgetItemId], false);
674
+ break;
675
+ }
676
+ case "create-placeholder-bill": {
677
+ const budgetItemId = positional[0];
678
+ if (!budgetItemId) {
679
+ throw new Error("create-placeholder-bill requires <budgetItemId>");
680
+ }
681
+ await createPlaceholderBillForBudgetItem(budgetItemId);
682
+ break;
683
+ }
559
684
  case "list-bills": {
560
685
  await listBills(parseListBillsFlags(flags));
561
686
  break;
@@ -583,6 +708,15 @@ async function main() {
583
708
  await createBillFromPayload(raw);
584
709
  break;
585
710
  }
711
+ case "validate-bill-selection": {
712
+ const payloadRaw = getFlag(flags, "payload");
713
+ if (!payloadRaw) {
714
+ throw new Error("validate-bill-selection requires --payload '<json>'");
715
+ }
716
+ const raw = parseJsonFlag(String(payloadRaw), "--payload");
717
+ await validateBillSelectionFromPayload(raw);
718
+ break;
719
+ }
586
720
  case "update-bill": {
587
721
  const payloadRaw = getFlag(flags, "payload");
588
722
  if (!payloadRaw) {
@@ -592,6 +726,15 @@ async function main() {
592
726
  await updateBillFromPayload(raw);
593
727
  break;
594
728
  }
729
+ case "update-bill-payment-evidence": {
730
+ const payloadRaw = getFlag(flags, "payload");
731
+ if (!payloadRaw) {
732
+ throw new Error("update-bill-payment-evidence requires --payload '<json>'");
733
+ }
734
+ const raw = parseJsonFlag(String(payloadRaw), "--payload");
735
+ await updateBillPaymentEvidenceFromPayload(raw);
736
+ break;
737
+ }
595
738
  case "delete-bill": {
596
739
  const id = positional[0];
597
740
  if (!id)
@@ -690,6 +833,35 @@ async function main() {
690
833
  await getBillAttachments(id);
691
834
  break;
692
835
  }
836
+ case "stage-bill-attachment": {
837
+ const [projectId, ...positionalFilePaths] = positional;
838
+ const fileFlag = getFlag(flags, "file");
839
+ const filesFlag = getFlag(flags, "files");
840
+ const filePaths = [
841
+ ...positionalFilePaths,
842
+ ...(fileFlag !== undefined ? [String(fileFlag)] : []),
843
+ ...(filesFlag !== undefined
844
+ ? String(filesFlag)
845
+ .split(",")
846
+ .map((value) => value.trim())
847
+ .filter(Boolean)
848
+ : []),
849
+ ];
850
+ if (!projectId || filePaths.length === 0) {
851
+ throw new Error("stage-bill-attachment requires <projectId> <filePath...> or --file/--files");
852
+ }
853
+ await stageBillAttachmentsFromPaths(projectId, filePaths);
854
+ break;
855
+ }
856
+ case "cleanup-staged-bill-attachments": {
857
+ const projectId = positional[0];
858
+ const keys = parseCommaSeparatedIds(getFlag(flags, "keys"));
859
+ if (!projectId || !keys) {
860
+ throw new Error("cleanup-staged-bill-attachments requires <projectId> --keys <csv>");
861
+ }
862
+ await cleanupStagedBillAttachments(projectId, keys);
863
+ break;
864
+ }
693
865
  case "upload-bill-attachment":
694
866
  case "attach-bill-attachment":
695
867
  case "attach-bill-payment-receipt": {
@@ -770,12 +942,9 @@ async function main() {
770
942
  projectId: getFlag(flags, "projectId"),
771
943
  budgetId: getFlag(flags, "budgetId"),
772
944
  supplierId: getFlag(flags, "supplierId"),
773
- status: statusRaw !== undefined
774
- ? String(statusRaw)
775
- .split(",")
776
- .map((value) => value.trim().toUpperCase())
777
- .filter(Boolean)
778
- : undefined,
945
+ status: parseCommaSeparatedQuotationStatuses(statusRaw),
946
+ requesterIds: parseCommaSeparatedIds(getFlag(flags, "requesterIds")),
947
+ search: getFlag(flags, "search"),
779
948
  page: getFlag(flags, "page") !== undefined
780
949
  ? Number(getFlag(flags, "page"))
781
950
  : undefined,
@@ -800,6 +969,15 @@ async function main() {
800
969
  await uploadQuotationAttachmentFromPath(projectId, filePath);
801
970
  break;
802
971
  }
972
+ case "cleanup-staged-quotation-attachments": {
973
+ const projectId = positional[0];
974
+ const keys = parseCommaSeparatedIds(getFlag(flags, "keys"));
975
+ if (!projectId || !keys) {
976
+ throw new Error("cleanup-staged-quotation-attachments requires <projectId> --keys <csv>");
977
+ }
978
+ await cleanupStagedQuotationAttachments(projectId, keys);
979
+ break;
980
+ }
803
981
  case "create-quotation": {
804
982
  const payloadRaw = getFlag(flags, "payload");
805
983
  if (!payloadRaw) {
@@ -809,6 +987,22 @@ async function main() {
809
987
  await createQuotationFromPayload(raw);
810
988
  break;
811
989
  }
990
+ case "update-quotation": {
991
+ const payloadRaw = getFlag(flags, "payload");
992
+ if (!payloadRaw) {
993
+ throw new Error("update-quotation requires --payload '<json>'");
994
+ }
995
+ const raw = parseJsonFlag(String(payloadRaw), "--payload");
996
+ await updateQuotationFromPayload(raw);
997
+ break;
998
+ }
999
+ case "delete-quotation": {
1000
+ const id = positional[0];
1001
+ if (!id)
1002
+ throw new Error("delete-quotation requires <quotationId>");
1003
+ await deleteQuotationById(id);
1004
+ break;
1005
+ }
812
1006
  case "submit-quotation": {
813
1007
  const id = positional[0];
814
1008
  if (!id)
@@ -842,10 +1036,114 @@ async function main() {
842
1036
  ? "ORIGINAL"
843
1037
  : roleRaw === "staff"
844
1038
  ? "STAFF_SIGNED"
845
- : "FINAL_SIGNED";
1039
+ : roleRaw === "final"
1040
+ ? "FINAL_SIGNED"
1041
+ : null;
1042
+ if (!fileRole) {
1043
+ throw new Error("download-quotation-pdf role must be original, staff, or final");
1044
+ }
846
1045
  await downloadQuotationPdf(id, fileRole);
847
1046
  break;
848
1047
  }
1048
+ case "check-customer-invoice-readiness": {
1049
+ const budgetId = positional[0];
1050
+ if (!budgetId) {
1051
+ throw new Error("check-customer-invoice-readiness requires <budgetId>");
1052
+ }
1053
+ await checkCustomerInvoiceReadiness(budgetId);
1054
+ break;
1055
+ }
1056
+ case "list-customer-invoices": {
1057
+ const budgetId = positional[0];
1058
+ if (!budgetId) {
1059
+ throw new Error("list-customer-invoices requires <budgetId>");
1060
+ }
1061
+ const statusRaw = getFlag(flags, "status");
1062
+ const sortDir = parseOptionalBillListSortDir(getFlag(flags, "sortDir"));
1063
+ await listCustomerInvoices({
1064
+ budgetId,
1065
+ page: parsePositiveIntFlag(getFlag(flags, "page"), "--page") ?? 1,
1066
+ perPage: parsePositiveIntFlag(getFlag(flags, "perPage"), "--perPage") ?? 12,
1067
+ q: getFlag(flags, "search") ?? "",
1068
+ statuses: parseCommaSeparatedCustomerInvoiceStatuses(statusRaw !== undefined ? String(statusRaw) : undefined),
1069
+ sortBy: parseCustomerInvoiceSortBy(getFlag(flags, "sortBy")),
1070
+ sortDir: sortDir ?? "desc",
1071
+ });
1072
+ break;
1073
+ }
1074
+ case "get-customer-invoice": {
1075
+ const batchId = positional[0];
1076
+ if (!batchId) {
1077
+ throw new Error("get-customer-invoice requires <batchId>");
1078
+ }
1079
+ await getCustomerInvoice(batchId);
1080
+ break;
1081
+ }
1082
+ case "create-customer-invoice": {
1083
+ const payloadRaw = getFlag(flags, "payload");
1084
+ if (!payloadRaw) {
1085
+ throw new Error("create-customer-invoice requires --payload '<json>'");
1086
+ }
1087
+ await createCustomerInvoice(parseJsonFlag(String(payloadRaw), "--payload"));
1088
+ break;
1089
+ }
1090
+ case "discard-customer-invoice": {
1091
+ const batchId = positional[0];
1092
+ if (!batchId) {
1093
+ throw new Error("discard-customer-invoice requires <batchId>");
1094
+ }
1095
+ await discardCreatingCustomerInvoice(batchId);
1096
+ break;
1097
+ }
1098
+ case "delete-customer-invoice": {
1099
+ const batchId = positional[0];
1100
+ if (!batchId) {
1101
+ throw new Error("delete-customer-invoice requires <batchId>");
1102
+ }
1103
+ await deleteCustomerInvoice(batchId);
1104
+ break;
1105
+ }
1106
+ case "void-customer-invoice": {
1107
+ const batchId = positional[0];
1108
+ if (!batchId) {
1109
+ throw new Error("void-customer-invoice requires <batchId>");
1110
+ }
1111
+ await voidCustomerInvoice(batchId);
1112
+ break;
1113
+ }
1114
+ case "approve-customer-invoice": {
1115
+ const batchId = positional[0];
1116
+ if (!batchId) {
1117
+ throw new Error("approve-customer-invoice requires <batchId>");
1118
+ }
1119
+ await approveCustomerInvoice(batchId);
1120
+ break;
1121
+ }
1122
+ case "reject-customer-invoice": {
1123
+ const batchId = positional[0];
1124
+ const reason = getFlag(flags, "reason");
1125
+ if (!batchId || reason === undefined) {
1126
+ throw new Error("reject-customer-invoice requires <batchId> --reason <text>");
1127
+ }
1128
+ await rejectCustomerInvoice(batchId, String(reason));
1129
+ break;
1130
+ }
1131
+ case "download-customer-invoice-pdf": {
1132
+ const invoiceId = positional[0];
1133
+ if (!invoiceId) {
1134
+ throw new Error("download-customer-invoice-pdf requires <invoiceId> [--output <path>]");
1135
+ }
1136
+ await downloadCustomerInvoicePdf(invoiceId, getFlag(flags, "output"));
1137
+ break;
1138
+ }
1139
+ case "sync-customer-invoice": {
1140
+ const invoiceId = positional[0];
1141
+ if (!invoiceId) {
1142
+ throw new Error("sync-customer-invoice requires <invoiceId>");
1143
+ }
1144
+ await syncCustomerInvoice(invoiceId);
1145
+ break;
1146
+ }
849
1147
  case "list-approvals":
850
1148
  case "get-pending-approvals": {
851
1149
  const typeRaw = getFlag(flags, "type");
@@ -976,6 +1274,7 @@ async function main() {
976
1274
  const contactPersonId = getFlag(flags, "contactPersonId");
977
1275
  const insideSalesId = getFlag(flags, "insideSalesId");
978
1276
  const businessDevelopmentId = getFlag(flags, "businessDevelopmentId");
1277
+ const projectManagerId = getFlag(flags, "projectManagerId");
979
1278
  const venue = getFlag(flags, "venue");
980
1279
  const pax = getFlag(flags, "pax");
981
1280
  const asanaTaskId = getFlag(flags, "asanaTaskId");
@@ -994,7 +1293,7 @@ async function main() {
994
1293
  !businessDevelopmentId ||
995
1294
  !venue ||
996
1295
  !startDate) {
997
- throw new Error("create-project requires --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --startDate (ISO/datetime flags as appropriate); optional --asanaTaskId, --asanaSearch, --pax and --endDate");
1296
+ throw new Error("create-project requires --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --startDate (ISO/datetime flags as appropriate); optional --projectManagerId, --asanaTaskId, --asanaSearch, --pax and --endDate");
998
1297
  }
999
1298
  await createProject({
1000
1299
  name,
@@ -1002,6 +1301,7 @@ async function main() {
1002
1301
  contactPersonId,
1003
1302
  insideSalesId,
1004
1303
  businessDevelopmentId,
1304
+ projectManagerId,
1005
1305
  venue,
1006
1306
  pax,
1007
1307
  asanaTaskId,
@@ -1031,17 +1331,50 @@ async function main() {
1031
1331
  await deleteProjectById(id);
1032
1332
  break;
1033
1333
  }
1334
+ case "check-project-reconciliation": {
1335
+ const id = positional[0];
1336
+ if (!id) {
1337
+ throw new Error("check-project-reconciliation requires <id>");
1338
+ }
1339
+ await checkProjectReconciliation(id);
1340
+ break;
1341
+ }
1342
+ case "reconcile-project": {
1343
+ const id = positional[0];
1344
+ if (!id)
1345
+ throw new Error("reconcile-project requires <id>");
1346
+ await reconcileProject(id);
1347
+ break;
1348
+ }
1349
+ case "complete-project": {
1350
+ const id = positional[0];
1351
+ if (!id)
1352
+ throw new Error("complete-project requires <id>");
1353
+ await completeProject(id);
1354
+ break;
1355
+ }
1356
+ case "import-qbo-project": {
1357
+ const projectId = positional[0];
1358
+ const projectUrl = getFlag(flags, "url");
1359
+ if (!projectId || projectUrl === undefined) {
1360
+ throw new Error("import-qbo-project requires <projectId> --url <quickBooksProjectUrl>");
1361
+ }
1362
+ await importQuickBooksProjectId(projectId, String(projectUrl));
1363
+ break;
1364
+ }
1034
1365
  case "update-project-status": {
1035
1366
  const [id, statusRaw] = positional;
1036
1367
  if (!id || !statusRaw) {
1037
1368
  throw new Error("update-project-status requires <id> <status>");
1038
1369
  }
1039
1370
  const pmIdFlag = getFlag(flags, "projectManagerId");
1040
- const pmNameFlag = getFlag(flags, "projectManagerName");
1371
+ const pmEmailFlag = getFlag(flags, "projectManagerEmail");
1041
1372
  const wonOverrideReasonFlag = getFlag(flags, "wonOverrideReason");
1042
1373
  await updateProjectStatus(id, parseProjectStatusForUpdate(statusRaw), {
1043
1374
  ...(pmIdFlag && { projectManagerId: String(pmIdFlag) }),
1044
- ...(pmNameFlag && { projectManagerName: String(pmNameFlag) }),
1375
+ ...(pmEmailFlag && {
1376
+ projectManagerEmail: String(pmEmailFlag),
1377
+ }),
1045
1378
  ...(wonOverrideReasonFlag && {
1046
1379
  wonOverrideReason: String(wonOverrideReasonFlag),
1047
1380
  }),
@@ -1316,6 +1649,24 @@ async function main() {
1316
1649
  await getErrorMetrics();
1317
1650
  break;
1318
1651
  }
1652
+ case "list-integration-operations": {
1653
+ await listIntegrationOperations({
1654
+ destination: getFlag(flags, "destination"),
1655
+ page: parsePositiveIntFlag(getFlag(flags, "page"), "--page") ?? 1,
1656
+ perPage: parsePositiveIntFlag(getFlag(flags, "perPage"), "--perPage") ?? 25,
1657
+ status: parseIntegrationOperationStatus(getFlag(flags, "status")),
1658
+ });
1659
+ break;
1660
+ }
1661
+ case "retry-integration-operation": {
1662
+ const operationId = positional[0];
1663
+ if (!operationId) {
1664
+ throw new Error("retry-integration-operation requires <operationId>");
1665
+ }
1666
+ await retryIntegrationOperation(operationId, parseOptionalBoolFlag(flags, "confirmExternalStateReconciled") ??
1667
+ false);
1668
+ break;
1669
+ }
1319
1670
  case "get-approved-budgets": {
1320
1671
  await getApprovedBudgets({
1321
1672
  projectId: getFlag(flags, "projectId"),