@go-labs-sg/bb 1.18.1 → 1.20.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, stageBillAttachmentsFromPaths, 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, 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";
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
+ estimate emails, customer invoices, integration retries, mark-budget-won,
153
+ submit-quotation, and supplier approval request side effects require an
154
+ interactive CONFIRM prompt 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.
@@ -144,6 +176,7 @@ Budgets
144
176
  delete-budget <budgetId>
145
177
  create-budget-approval <budgetId> (also sends approval request emails)
146
178
  create-estimate <budgetId> (creates QuickBooks estimate; supports preflight updates)
179
+ send-estimate-to-contact-person --payload '<json>' Send using the web contact-person workflow; payload includes recipient fields plus HTML content/signature.
147
180
  add-budget-items --budgetId --items '[{"itemId":"…","quantity":1,"markup":30},…]'
148
181
  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
182
  remove-budget-item <budgetItemId>
@@ -154,6 +187,11 @@ Budgets
154
187
  delete-budget-category <id>
155
188
  update-budget-commission --payload '<json>' (budget.updateBudgetCommission)
156
189
  update-budget-discount --payload '<json>' (budget.updateBudgetDiscount; type percentage|amount)
190
+ delete-budget-commission <budgetId> Remove commission and return the budget to DRAFT.
191
+ delete-budget-discount <budgetId> Remove discount and return the budget to DRAFT.
192
+ mark-budget-items-not-utilized --ids <csv> Remove unused lines from reconciliation and clear QBO placeholders.
193
+ restore-budget-item <budgetItemId> Restore one Not Utilized line and its QBO placeholder when required.
194
+ create-placeholder-bill <budgetItemId> Admin-only placeholder recovery.
157
195
  upload-budget-attachment <budgetId> <filePath> Presigned S3 upload + confirm
158
196
 
159
197
  Bills
@@ -163,12 +201,16 @@ Bills
163
201
  New supplier bills from 1 Jul 2026 00:00 SGT require approved quotation coverage for each non-legacy line item.
164
202
  create-bill-approval <billId> (also sends approval request emails)
165
203
  create-bill --payload '<json>' (bill.create; payload.isClaimable false = bill, true = claim)
204
+ validate-bill-selection --payload '<json>' Check projectId, supplierId, budgetItemIds, and optional alreadyPaid before creation.
166
205
  stage-bill-attachment <projectId> <filePath...> [--file <path>] [--files <csv>] Upload files before create-bill; returns attachment JSON for attachments/paymentProofAttachments
206
+ cleanup-staged-bill-attachments <projectId> --keys <csv> Delete unattached staged bill/claim uploads.
167
207
  update-bill --payload '<json>' (bill.update; must include id)
208
+ update-bill-payment-evidence --payload '<json>' Replace paymentReference/paymentProofAttachments for an already-paid bill.
168
209
  delete-bill <billId>
169
- update-bill-status <id> <status> [--rejectionReason] [--paymentTrackingUrl] [--paymentReference]
210
+ update-bill-status <id> <status> [--rejectionReason] [--paymentTrackingUrl] [--paymentReference] [--paymentProof <receipt.pdf>]
170
211
  status (${billStatusesForUpdateHelp.join(", ")}) — not PENDING_APPROVAL; use create-bill-approval
171
- 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.
212
+ PAID requires --paymentReference and a payment-proof PDF. Use --paymentProof to stage and submit the PDF atomically, or omit it only when BB already has a BILL_PAYMENT_PROOF attachment.
213
+ Moving a bill or claim to PAID runs the server's paid-bill notification workflow.
172
214
  patch-bill-payment <billId> [--paymentTrackingUrl] [--paymentReference] [--quickbooksBillId] [--paymentDate <ISO>]
173
215
  Clear a field: --clearPaymentTrackingUrl true | --clearPaymentReference true | --clearQuickbooksBillId true | --clearPaymentDate true
174
216
  patch-bill-invoice-number <billId> <invoiceNumber>
@@ -179,17 +221,33 @@ Bills
179
221
  get-bill-details <billId>
180
222
 
181
223
  Quotations
182
- list-quotations [--projectId] [--budgetId] [--supplierId] [--status CSV] [--page] [--pageSize]
224
+ list-quotations [--projectId] [--budgetId] [--supplierId] [--status CSV] [--requesterIds CSV] [--search] [--page] [--pageSize]
183
225
  get-quotation <quotationId>
184
- upload-quotation-attachment <projectId> <filePath> Presigned S3 upload; returns attachment JSON for create-quotation payload
226
+ upload-quotation-attachment <projectId> <filePath> Upload one PDF, JPEG, or PNG up to 20MB; returns attachment JSON for a quotation payload.
227
+ cleanup-staged-quotation-attachments <projectId> --keys <csv> Delete unattached staged quotation uploads.
185
228
  create-quotation --payload '<json>' (quotation.createDraft; requires amountWithoutGst and gstAmount)
229
+ update-quotation --payload '<json>' Update a DRAFT or REJECTED quotation; same payload plus id.
230
+ delete-quotation <quotationId>
186
231
  submit-quotation <quotationId>
187
232
  approve-quotation <quotationId>
188
233
  reject-quotation <quotationId> --reason <text>
189
234
  download-quotation-pdf <quotationId> [original|staff|final]
190
235
 
236
+ Customer invoices
237
+ check-customer-invoice-readiness <budgetId>
238
+ list-customer-invoices <budgetId> [--status CSV] [--search] [--sortBy createdAt|status|totalNetAmount|approvalExpiresAt] [--sortDir asc|desc] [--page] [--perPage]
239
+ get-customer-invoice <batchId>
240
+ create-customer-invoice --payload '<json>' budgetId + one split with label, percentage, and dueDate.
241
+ discard-customer-invoice <batchId> Discard a reserved CREATING batch with no created QBO invoices.
242
+ delete-customer-invoice <batchId> Delete QBO invoices and the local batch.
243
+ void-customer-invoice <batchId> Void the invoice batch in QuickBooks.
244
+ approve-customer-invoice <batchId> Admin approval; notifies the creator.
245
+ reject-customer-invoice <batchId> --reason <text> Admin rejection; voids QBO invoices and notifies the creator.
246
+ download-customer-invoice-pdf <invoiceId> [--output <path>]
247
+ sync-customer-invoice <invoiceId> Refresh local status and balance from QuickBooks.
248
+
191
249
  Approvals
192
- list-approvals | get-pending-approvals [--type budget|supplier|bill|quotation|all]
250
+ list-approvals | get-pending-approvals [--type budget|supplier|bill|quotation|customer_invoice|all]
193
251
  approve-bill <billId> (also sends requester reply email)
194
252
  reject-bill <billId> --reason <text> (also sends requester reply email)
195
253
  approve-budget <budgetId> (also sends requester reply email)
@@ -211,8 +269,12 @@ Companies & projects
211
269
  create-project --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --startDate <ISO> [--projectManagerId <userId>] [--asanaTaskId] [--asanaSearch <text>] [--slackChannelId --slackChannelUrl --slackChannelName] [--pax] [--endDate <ISO>] [--description]
212
270
  update-project --payload '<json>' (project.updateProject; optional projectManagerId and requestQboAccountantNotification; default notify like web)
213
271
  delete-project <id>
272
+ check-project-reconciliation <id> Run the same live reconciliation review as the web app without changing status.
273
+ reconcile-project <id> Rerun all checks transactionally and mark an eligible WON project RECONCILED.
274
+ complete-project <id> Rerun reconciliation validation, mark a RECONCILED project COMPLETED, and queue QBO placeholder cleanup.
275
+ import-qbo-project <projectId> --url <quickBooksProjectUrl>
214
276
  update-project-status <id> <status> [--projectManagerId <userId>] [--projectManagerEmail <email>] [--wonOverrideReason <text>]
215
- Required when moving PITCH → WON: supply --projectManagerId or --projectManagerEmail.
277
+ Required when moving PITCH → WON: supply --projectManagerId or --projectManagerEmail.
216
278
  Required when moving to WON without accepted/closed budget or win proof: --wonOverrideReason (10+ chars).
217
279
  status: ${projectStatusesForHelp.join(", ")}
218
280
 
@@ -261,6 +323,10 @@ Errors
261
323
  get-recent-errors [--page] [--perPage] [--severity] [--status]
262
324
  get-error-metrics
263
325
 
326
+ Automation (admin)
327
+ list-integration-operations [--destination] [--status PENDING|PROCESSING|FAILED|COMPLETED] [--page] [--perPage]
328
+ retry-integration-operation <operationId> [--confirmExternalStateReconciled true|false]
329
+
264
330
  Historical / benchmarks
265
331
  get-approved-budgets [--projectId] [--limit] [--minRevenue] [--maxRevenue]
266
332
  get-budget-category-benchmarks <categoryId> [--limit]
@@ -377,6 +443,23 @@ async function main() {
377
443
  await getBudgetVersions(id);
378
444
  break;
379
445
  }
446
+ case "rename-budget-version": {
447
+ const versionId = positional[0];
448
+ const newName = getFlag(flags, "name");
449
+ if (!versionId || newName === undefined) {
450
+ throw new Error("rename-budget-version requires <versionId> --name <text>");
451
+ }
452
+ await renameBudgetVersion(versionId, String(newName));
453
+ break;
454
+ }
455
+ case "restore-budget-version": {
456
+ const versionId = positional[0];
457
+ if (!versionId) {
458
+ throw new Error("restore-budget-version requires <versionId>");
459
+ }
460
+ await restoreBudgetVersion(versionId);
461
+ break;
462
+ }
380
463
  case "update-budget-status": {
381
464
  const [id, status] = positional;
382
465
  if (!id || !status) {
@@ -434,6 +517,16 @@ async function main() {
434
517
  });
435
518
  break;
436
519
  }
520
+ case "send-estimate-email":
521
+ case "send-estimate-to-contact-person": {
522
+ const payloadRaw = getFlag(flags, "payload");
523
+ if (!payloadRaw) {
524
+ throw new Error("send-estimate-to-contact-person requires --payload '<json>'");
525
+ }
526
+ const raw = parseJsonFlag(String(payloadRaw), "--payload");
527
+ await sendEstimateToContactPersonFromPayload(raw);
528
+ break;
529
+ }
437
530
  case "create-budget": {
438
531
  const payloadRaw = getFlag(flags, "payload");
439
532
  if (!payloadRaw) {
@@ -560,6 +653,46 @@ async function main() {
560
653
  await updateBudgetDiscountFromPayload(raw);
561
654
  break;
562
655
  }
656
+ case "delete-budget-commission": {
657
+ const budgetId = positional[0];
658
+ if (!budgetId) {
659
+ throw new Error("delete-budget-commission requires <budgetId>");
660
+ }
661
+ await deleteBudgetCommission(budgetId);
662
+ break;
663
+ }
664
+ case "delete-budget-discount": {
665
+ const budgetId = positional[0];
666
+ if (!budgetId) {
667
+ throw new Error("delete-budget-discount requires <budgetId>");
668
+ }
669
+ await deleteBudgetDiscount(budgetId);
670
+ break;
671
+ }
672
+ case "mark-budget-items-not-utilized": {
673
+ const budgetItemIds = parseCommaSeparatedIds(getFlag(flags, "ids"));
674
+ if (!budgetItemIds?.length) {
675
+ throw new Error("mark-budget-items-not-utilized requires --ids <csv>");
676
+ }
677
+ await setBudgetItemsNotUtilized(budgetItemIds, true);
678
+ break;
679
+ }
680
+ case "restore-budget-item": {
681
+ const budgetItemId = positional[0];
682
+ if (!budgetItemId) {
683
+ throw new Error("restore-budget-item requires <budgetItemId>");
684
+ }
685
+ await setBudgetItemsNotUtilized([budgetItemId], false);
686
+ break;
687
+ }
688
+ case "create-placeholder-bill": {
689
+ const budgetItemId = positional[0];
690
+ if (!budgetItemId) {
691
+ throw new Error("create-placeholder-bill requires <budgetItemId>");
692
+ }
693
+ await createPlaceholderBillForBudgetItem(budgetItemId);
694
+ break;
695
+ }
563
696
  case "list-bills": {
564
697
  await listBills(parseListBillsFlags(flags));
565
698
  break;
@@ -587,6 +720,15 @@ async function main() {
587
720
  await createBillFromPayload(raw);
588
721
  break;
589
722
  }
723
+ case "validate-bill-selection": {
724
+ const payloadRaw = getFlag(flags, "payload");
725
+ if (!payloadRaw) {
726
+ throw new Error("validate-bill-selection requires --payload '<json>'");
727
+ }
728
+ const raw = parseJsonFlag(String(payloadRaw), "--payload");
729
+ await validateBillSelectionFromPayload(raw);
730
+ break;
731
+ }
590
732
  case "update-bill": {
591
733
  const payloadRaw = getFlag(flags, "payload");
592
734
  if (!payloadRaw) {
@@ -596,6 +738,15 @@ async function main() {
596
738
  await updateBillFromPayload(raw);
597
739
  break;
598
740
  }
741
+ case "update-bill-payment-evidence": {
742
+ const payloadRaw = getFlag(flags, "payload");
743
+ if (!payloadRaw) {
744
+ throw new Error("update-bill-payment-evidence requires --payload '<json>'");
745
+ }
746
+ const raw = parseJsonFlag(String(payloadRaw), "--payload");
747
+ await updateBillPaymentEvidenceFromPayload(raw);
748
+ break;
749
+ }
599
750
  case "delete-bill": {
600
751
  const id = positional[0];
601
752
  if (!id)
@@ -606,7 +757,11 @@ async function main() {
606
757
  case "update-bill-status": {
607
758
  const [id, statusRaw] = positional;
608
759
  if (!id || !statusRaw) {
609
- throw new Error("update-bill-status requires <billId> <status> [--rejectionReason] [--paymentTrackingUrl] [--paymentReference]");
760
+ throw new Error("update-bill-status requires <billId> <status> [--rejectionReason] [--paymentTrackingUrl] [--paymentReference] [--paymentProof <receipt.pdf>]");
761
+ }
762
+ const paymentProofFlag = flags.paymentProof ?? flags["payment-proof"] ?? flags.receipt;
763
+ if (paymentProofFlag === true) {
764
+ throw new Error("--paymentProof requires a PDF file path.");
610
765
  }
611
766
  const status = parseBillStatusForUpdate(statusRaw);
612
767
  await updateBillStatus({
@@ -615,6 +770,7 @@ async function main() {
615
770
  rejectionReason: getFlag(flags, "rejectionReason"),
616
771
  paymentTrackingUrl: getFlag(flags, "paymentTrackingUrl"),
617
772
  paymentReference: getFlag(flags, "paymentReference"),
773
+ paymentProofPath: paymentProofFlag,
618
774
  });
619
775
  break;
620
776
  }
@@ -714,6 +870,15 @@ async function main() {
714
870
  await stageBillAttachmentsFromPaths(projectId, filePaths);
715
871
  break;
716
872
  }
873
+ case "cleanup-staged-bill-attachments": {
874
+ const projectId = positional[0];
875
+ const keys = parseCommaSeparatedIds(getFlag(flags, "keys"));
876
+ if (!projectId || !keys) {
877
+ throw new Error("cleanup-staged-bill-attachments requires <projectId> --keys <csv>");
878
+ }
879
+ await cleanupStagedBillAttachments(projectId, keys);
880
+ break;
881
+ }
717
882
  case "upload-bill-attachment":
718
883
  case "attach-bill-attachment":
719
884
  case "attach-bill-payment-receipt": {
@@ -794,12 +959,9 @@ async function main() {
794
959
  projectId: getFlag(flags, "projectId"),
795
960
  budgetId: getFlag(flags, "budgetId"),
796
961
  supplierId: getFlag(flags, "supplierId"),
797
- status: statusRaw !== undefined
798
- ? String(statusRaw)
799
- .split(",")
800
- .map((value) => value.trim().toUpperCase())
801
- .filter(Boolean)
802
- : undefined,
962
+ status: parseCommaSeparatedQuotationStatuses(statusRaw),
963
+ requesterIds: parseCommaSeparatedIds(getFlag(flags, "requesterIds")),
964
+ search: getFlag(flags, "search"),
803
965
  page: getFlag(flags, "page") !== undefined
804
966
  ? Number(getFlag(flags, "page"))
805
967
  : undefined,
@@ -824,6 +986,15 @@ async function main() {
824
986
  await uploadQuotationAttachmentFromPath(projectId, filePath);
825
987
  break;
826
988
  }
989
+ case "cleanup-staged-quotation-attachments": {
990
+ const projectId = positional[0];
991
+ const keys = parseCommaSeparatedIds(getFlag(flags, "keys"));
992
+ if (!projectId || !keys) {
993
+ throw new Error("cleanup-staged-quotation-attachments requires <projectId> --keys <csv>");
994
+ }
995
+ await cleanupStagedQuotationAttachments(projectId, keys);
996
+ break;
997
+ }
827
998
  case "create-quotation": {
828
999
  const payloadRaw = getFlag(flags, "payload");
829
1000
  if (!payloadRaw) {
@@ -833,6 +1004,22 @@ async function main() {
833
1004
  await createQuotationFromPayload(raw);
834
1005
  break;
835
1006
  }
1007
+ case "update-quotation": {
1008
+ const payloadRaw = getFlag(flags, "payload");
1009
+ if (!payloadRaw) {
1010
+ throw new Error("update-quotation requires --payload '<json>'");
1011
+ }
1012
+ const raw = parseJsonFlag(String(payloadRaw), "--payload");
1013
+ await updateQuotationFromPayload(raw);
1014
+ break;
1015
+ }
1016
+ case "delete-quotation": {
1017
+ const id = positional[0];
1018
+ if (!id)
1019
+ throw new Error("delete-quotation requires <quotationId>");
1020
+ await deleteQuotationById(id);
1021
+ break;
1022
+ }
836
1023
  case "submit-quotation": {
837
1024
  const id = positional[0];
838
1025
  if (!id)
@@ -866,10 +1053,114 @@ async function main() {
866
1053
  ? "ORIGINAL"
867
1054
  : roleRaw === "staff"
868
1055
  ? "STAFF_SIGNED"
869
- : "FINAL_SIGNED";
1056
+ : roleRaw === "final"
1057
+ ? "FINAL_SIGNED"
1058
+ : null;
1059
+ if (!fileRole) {
1060
+ throw new Error("download-quotation-pdf role must be original, staff, or final");
1061
+ }
870
1062
  await downloadQuotationPdf(id, fileRole);
871
1063
  break;
872
1064
  }
1065
+ case "check-customer-invoice-readiness": {
1066
+ const budgetId = positional[0];
1067
+ if (!budgetId) {
1068
+ throw new Error("check-customer-invoice-readiness requires <budgetId>");
1069
+ }
1070
+ await checkCustomerInvoiceReadiness(budgetId);
1071
+ break;
1072
+ }
1073
+ case "list-customer-invoices": {
1074
+ const budgetId = positional[0];
1075
+ if (!budgetId) {
1076
+ throw new Error("list-customer-invoices requires <budgetId>");
1077
+ }
1078
+ const statusRaw = getFlag(flags, "status");
1079
+ const sortDir = parseOptionalBillListSortDir(getFlag(flags, "sortDir"));
1080
+ await listCustomerInvoices({
1081
+ budgetId,
1082
+ page: parsePositiveIntFlag(getFlag(flags, "page"), "--page") ?? 1,
1083
+ perPage: parsePositiveIntFlag(getFlag(flags, "perPage"), "--perPage") ?? 12,
1084
+ q: getFlag(flags, "search") ?? "",
1085
+ statuses: parseCommaSeparatedCustomerInvoiceStatuses(statusRaw !== undefined ? String(statusRaw) : undefined),
1086
+ sortBy: parseCustomerInvoiceSortBy(getFlag(flags, "sortBy")),
1087
+ sortDir: sortDir ?? "desc",
1088
+ });
1089
+ break;
1090
+ }
1091
+ case "get-customer-invoice": {
1092
+ const batchId = positional[0];
1093
+ if (!batchId) {
1094
+ throw new Error("get-customer-invoice requires <batchId>");
1095
+ }
1096
+ await getCustomerInvoice(batchId);
1097
+ break;
1098
+ }
1099
+ case "create-customer-invoice": {
1100
+ const payloadRaw = getFlag(flags, "payload");
1101
+ if (!payloadRaw) {
1102
+ throw new Error("create-customer-invoice requires --payload '<json>'");
1103
+ }
1104
+ await createCustomerInvoice(parseJsonFlag(String(payloadRaw), "--payload"));
1105
+ break;
1106
+ }
1107
+ case "discard-customer-invoice": {
1108
+ const batchId = positional[0];
1109
+ if (!batchId) {
1110
+ throw new Error("discard-customer-invoice requires <batchId>");
1111
+ }
1112
+ await discardCreatingCustomerInvoice(batchId);
1113
+ break;
1114
+ }
1115
+ case "delete-customer-invoice": {
1116
+ const batchId = positional[0];
1117
+ if (!batchId) {
1118
+ throw new Error("delete-customer-invoice requires <batchId>");
1119
+ }
1120
+ await deleteCustomerInvoice(batchId);
1121
+ break;
1122
+ }
1123
+ case "void-customer-invoice": {
1124
+ const batchId = positional[0];
1125
+ if (!batchId) {
1126
+ throw new Error("void-customer-invoice requires <batchId>");
1127
+ }
1128
+ await voidCustomerInvoice(batchId);
1129
+ break;
1130
+ }
1131
+ case "approve-customer-invoice": {
1132
+ const batchId = positional[0];
1133
+ if (!batchId) {
1134
+ throw new Error("approve-customer-invoice requires <batchId>");
1135
+ }
1136
+ await approveCustomerInvoice(batchId);
1137
+ break;
1138
+ }
1139
+ case "reject-customer-invoice": {
1140
+ const batchId = positional[0];
1141
+ const reason = getFlag(flags, "reason");
1142
+ if (!batchId || reason === undefined) {
1143
+ throw new Error("reject-customer-invoice requires <batchId> --reason <text>");
1144
+ }
1145
+ await rejectCustomerInvoice(batchId, String(reason));
1146
+ break;
1147
+ }
1148
+ case "download-customer-invoice-pdf": {
1149
+ const invoiceId = positional[0];
1150
+ if (!invoiceId) {
1151
+ throw new Error("download-customer-invoice-pdf requires <invoiceId> [--output <path>]");
1152
+ }
1153
+ await downloadCustomerInvoicePdf(invoiceId, getFlag(flags, "output"));
1154
+ break;
1155
+ }
1156
+ case "sync-customer-invoice": {
1157
+ const invoiceId = positional[0];
1158
+ if (!invoiceId) {
1159
+ throw new Error("sync-customer-invoice requires <invoiceId>");
1160
+ }
1161
+ await syncCustomerInvoice(invoiceId);
1162
+ break;
1163
+ }
873
1164
  case "list-approvals":
874
1165
  case "get-pending-approvals": {
875
1166
  const typeRaw = getFlag(flags, "type");
@@ -1057,6 +1348,37 @@ async function main() {
1057
1348
  await deleteProjectById(id);
1058
1349
  break;
1059
1350
  }
1351
+ case "check-project-reconciliation": {
1352
+ const id = positional[0];
1353
+ if (!id) {
1354
+ throw new Error("check-project-reconciliation requires <id>");
1355
+ }
1356
+ await checkProjectReconciliation(id);
1357
+ break;
1358
+ }
1359
+ case "reconcile-project": {
1360
+ const id = positional[0];
1361
+ if (!id)
1362
+ throw new Error("reconcile-project requires <id>");
1363
+ await reconcileProject(id);
1364
+ break;
1365
+ }
1366
+ case "complete-project": {
1367
+ const id = positional[0];
1368
+ if (!id)
1369
+ throw new Error("complete-project requires <id>");
1370
+ await completeProject(id);
1371
+ break;
1372
+ }
1373
+ case "import-qbo-project": {
1374
+ const projectId = positional[0];
1375
+ const projectUrl = getFlag(flags, "url");
1376
+ if (!projectId || projectUrl === undefined) {
1377
+ throw new Error("import-qbo-project requires <projectId> --url <quickBooksProjectUrl>");
1378
+ }
1379
+ await importQuickBooksProjectId(projectId, String(projectUrl));
1380
+ break;
1381
+ }
1060
1382
  case "update-project-status": {
1061
1383
  const [id, statusRaw] = positional;
1062
1384
  if (!id || !statusRaw) {
@@ -1344,6 +1666,24 @@ async function main() {
1344
1666
  await getErrorMetrics();
1345
1667
  break;
1346
1668
  }
1669
+ case "list-integration-operations": {
1670
+ await listIntegrationOperations({
1671
+ destination: getFlag(flags, "destination"),
1672
+ page: parsePositiveIntFlag(getFlag(flags, "page"), "--page") ?? 1,
1673
+ perPage: parsePositiveIntFlag(getFlag(flags, "perPage"), "--perPage") ?? 25,
1674
+ status: parseIntegrationOperationStatus(getFlag(flags, "status")),
1675
+ });
1676
+ break;
1677
+ }
1678
+ case "retry-integration-operation": {
1679
+ const operationId = positional[0];
1680
+ if (!operationId) {
1681
+ throw new Error("retry-integration-operation requires <operationId>");
1682
+ }
1683
+ await retryIntegrationOperation(operationId, parseOptionalBoolFlag(flags, "confirmExternalStateReconciled") ??
1684
+ false);
1685
+ break;
1686
+ }
1347
1687
  case "get-approved-budgets": {
1348
1688
  await getApprovedBudgets({
1349
1689
  projectId: getFlag(flags, "projectId"),
@@ -1,8 +1,10 @@
1
1
  import { BudgetRole, Deals, ExtendedApprovalStatus, ExtendedBillStatus, ExtendedErrorSeverity, ExtendedErrorStatus, ExtendedProjectStatus, TimeFrame, } from "./filter-enums.js";
2
- import { BillStatus, BudgetStatus, ProjectStatus, UserRole, } from "./prisma-enums.js";
2
+ import { BillStatus, BudgetStatus, CustomerInvoiceBatchStatus, ProjectStatus, SupplierQuotationStatus, UserRole, } from "./prisma-enums.js";
3
3
  const BUDGET_STATUS_VALUES = new Set(Object.values(BudgetStatus));
4
4
  const BILL_STATUS_VALUES = new Set(Object.values(BillStatus));
5
5
  const PROJECT_STATUS_VALUES = new Set(Object.values(ProjectStatus));
6
+ const SUPPLIER_QUOTATION_STATUS_VALUES = new Set(Object.values(SupplierQuotationStatus));
7
+ const CUSTOMER_INVOICE_BATCH_STATUS_VALUES = new Set(Object.values(CustomerInvoiceBatchStatus));
6
8
  const USER_ROLE_VALUES = new Set(Object.values(UserRole));
7
9
  const EXTENDED_PROJECT_STATUS_VALUES = new Set(Object.values(ExtendedProjectStatus));
8
10
  const DASHBOARD_ROLE_VALUES = new Set([
@@ -173,6 +175,38 @@ export function parseCommaSeparatedSupplierStatuses(raw) {
173
175
  }
174
176
  return out;
175
177
  }
178
+ export function parseCommaSeparatedQuotationStatuses(raw) {
179
+ if (raw === undefined)
180
+ return undefined;
181
+ const parts = raw
182
+ .split(",")
183
+ .map((value) => value.trim().toUpperCase())
184
+ .filter(Boolean);
185
+ if (parts.length === 0)
186
+ return undefined;
187
+ for (const status of parts) {
188
+ if (!SUPPLIER_QUOTATION_STATUS_VALUES.has(status)) {
189
+ throw new Error(`Invalid quotation status "${status}". Use one of: ${[...SUPPLIER_QUOTATION_STATUS_VALUES].join(", ")}.`);
190
+ }
191
+ }
192
+ return parts;
193
+ }
194
+ export function parseCommaSeparatedCustomerInvoiceStatuses(raw) {
195
+ if (raw === undefined)
196
+ return undefined;
197
+ const parts = raw
198
+ .split(",")
199
+ .map((value) => value.trim().toUpperCase())
200
+ .filter(Boolean);
201
+ if (parts.length === 0)
202
+ return undefined;
203
+ for (const status of parts) {
204
+ if (!CUSTOMER_INVOICE_BATCH_STATUS_VALUES.has(status)) {
205
+ throw new Error(`Invalid customer invoice status "${status}". Use one of: ${[...CUSTOMER_INVOICE_BATCH_STATUS_VALUES].join(", ")}.`);
206
+ }
207
+ }
208
+ return parts;
209
+ }
176
210
  export function parseBillStatusFlag(raw) {
177
211
  if (raw === undefined)
178
212
  return undefined;
@@ -225,7 +259,11 @@ export function parseOptionalBillListSortDir(raw) {
225
259
  return raw;
226
260
  }
227
261
  function isPendingRequestType(s) {
228
- return (s === "BUDGET" || s === "BILL" || s === "SUPPLIER" || s === "QUOTATION");
262
+ return (s === "BUDGET" ||
263
+ s === "BILL" ||
264
+ s === "SUPPLIER" ||
265
+ s === "QUOTATION" ||
266
+ s === "CUSTOMER_INVOICE");
229
267
  }
230
268
  export function parseApprovalTypeFlag(raw) {
231
269
  if (raw === undefined)
@@ -234,7 +272,7 @@ export function parseApprovalTypeFlag(raw) {
234
272
  if (u === "ALL")
235
273
  return "ALL";
236
274
  if (!isPendingRequestType(u)) {
237
- throw new Error(`Invalid --type "${raw}". Use budget, supplier, bill, quotation, or all.`);
275
+ throw new Error(`Invalid --type "${raw}". Use budget, supplier, bill, quotation, customer_invoice, or all.`);
238
276
  }
239
277
  return u;
240
278
  }