@go-labs-sg/bb 1.18.1 → 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/README.md CHANGED
@@ -6,7 +6,7 @@ For AI agents: bills and claims are stored as the same underlying bill records.
6
6
 
7
7
  For AI agents using this CLI: always check whether a newer `@go-labs-sg/bb` version is available before running Budget Builder commands, then update the CLI if needed so command flags and API behavior stay current.
8
8
 
9
- For AI agents using this CLI: do not execute approval decisions, approval requests, status updates, `mark-budget-won`, `submit-quotation`, or supplier approval request side effects until the user explicitly confirms the exact entity, target state, and side effects. Inspect the entity first, summarize what will change and whether emails/integrations will run, then wait for a clear confirmation from the user. These sensitive workflow commands also require an interactive `CONFIRM` prompt at runtime and abort in non-interactive shells.
9
+ For AI agents using this CLI: do not execute approval decisions, approval requests, status updates, customer-invoice changes, integration retries, `mark-budget-won`, `submit-quotation`, or supplier approval request side effects until the user explicitly confirms the exact entity, target state, and side effects. Inspect the entity first, summarize what will change and whether emails/integrations will run, then wait for a clear confirmation from the user. These sensitive workflow commands also require an interactive `CONFIRM` prompt at runtime and abort in non-interactive shells.
10
10
 
11
11
  **Registry:** [`@go-labs-sg/bb`](https://www.npmjs.com/package/@go-labs-sg/bb)
12
12
 
@@ -72,7 +72,7 @@ Global options and flags use `--key=value` or `--key value` (see `bb help`).
72
72
 
73
73
  **Budget status automation:** Setting a budget to `ESTIMATE_ACCEPTED` requires a confirmed win-proof attachment. If the parent project is `PITCH` or `LOST`, the API now marks it `WON` automatically and, when the `asana-tasks` feature flag is enabled, creates an Asana Event Ops section from Prompt 5 tasks. Prompt 5 skips quantity-zero items, combines matching non-GO-internal suppliers, keeps each GO internal item separate, removes generated work that repeats another line item or group, and schedules applicable subtasks relative to the project start date. When rejecting the only accepted/closed budget on a commercial project, pass `--projectStatusOnCommercialRejection PITCH|LOST`.
74
74
 
75
- **Sensitive workflow changes:** Approval decisions/requests, entity status updates, `mark-budget-won`, `submit-quotation`, and supplier approval request side effects require an interactive `CONFIRM` prompt. Non-interactive runs abort before the guarded workflow mutation. Agents must get user confirmation in chat before attempting the command; the prompt is a final runtime guard, not a replacement for user approval.
75
+ **Sensitive workflow changes:** Approval decisions/requests, entity status updates, customer-invoice changes, integration retries, `mark-budget-won`, `submit-quotation`, and supplier approval request side effects require an interactive `CONFIRM` prompt. Non-interactive runs abort before the guarded workflow mutation. Agents must get user confirmation in chat before attempting the command; the prompt is a final runtime guard, not a replacement for user approval.
76
76
 
77
77
  **Approval email exclusions:** Approval requests create pending database records for every eligible approver, including configured non-recipient admin accounts. Automated approval-request emails skip those accounts.
78
78
 
@@ -84,17 +84,21 @@ Global options and flags use `--key=value` or `--key value` (see `bb help`).
84
84
 
85
85
  | Area | Commands (non-exhaustive) |
86
86
  | --- | --- |
87
- | **Budgets** | `list-budgets` (full payload by default; `--summary` or `--includeDetails false` for slim list), `get-budget`, `get-budget-items`, `get-budget-details`, `get-budget-categories`, `get-budget-versions`, `update-budget-status` (`ESTIMATE_ACCEPTED` requires win proof, auto-marks `PITCH` or `LOST` projects `WON`, and auto-creates an Asana Event Ops section from Prompt 5 tasks when enabled; Prompt 5 skips quantity-zero items, combines matching non-GO-internal suppliers, keeps each GO internal item separate, removes generated work that repeats another line item or group, and schedules applicable subtasks relative to the project start date; `--projectStatusOnCommercialRejection PITCH\|LOST` is required when rejecting the only accepted/closed budget on a commercial project), `mark-budget-won` (`<budgetId>` + proof file path; `PITCH` or `LOST` projects become `WON` automatically), `create-budget` / `update-budget` (`--payload`), `delete-budget`, `create-budget-approval` (also sends approval request emails), `add-budget-items`, `update-budget-item`, `remove-budget-item`, `reorder-budget-items`, `update-budget-item-supplier`, `create-budget-category`, `update-budget-category`, `delete-budget-category`, `update-budget-commission`, `update-budget-discount` (`--payload` where noted), `upload-budget-attachment` (`<budgetId>` + local file path; uses `attachment.requestBudgetAttachmentUpload` + PUT + `attachment.confirmBudgetAttachment`) |
88
- | **Bills / claims** | `list-bills` (`--isClaimable false` for bills, `--isClaimable true` for claims, omit for both), `list-claims` (claims only), `stage-bill-attachment` (securely uploads invoice/payment-proof files before creation and returns attachment JSON), `create-bill` (`--payload`; set `isClaimable=false` for a bill, `isClaimable=true` for a claim; supplier bills require positive `extractedAmount` and `amount`, with `amount <= extractedAmount`; an already-paid supplier bill sets `alreadyPaid=true` and requires `paymentReference` plus a staged PDF in `paymentProofAttachments`, and is created pending finance approval; non-legacy supplier bills from 1 Jul 2026 00:00 SGT also require approved quotation coverage), `update-bill` (`--payload`), `delete-bill`, `create-bill-approval` (also sends approval request emails), `update-bill-status` (moving a bill or claim to `PAID` sends creator and supplier emails, CC `m@getout.sg` and BCC `noah@getout.sg`; a claim sends one email when both recipient addresses match), `patch-bill-payment` (PAID bills: `--paymentTrackingUrl`, `--paymentReference`, `--quickbooksBillId`, `--paymentDate` ISO; clearing a paid bill's QuickBooks link is not allowed), `patch-bill-invoice-number`, `get-bill-attachments`, `upload-bill-attachment` (`<billId>` + local path), `get-bill-details` |
89
- | **Quotations** | `list-quotations`, `get-quotation`, `upload-quotation-attachment` (`<projectId>` + local PDF path; returns attachment JSON for payload use), `create-quotation` (`--payload` for `quotation.createDraft`; include `amountWithoutGst` and `gstAmount`), `submit-quotation`, `approve-quotation`, `reject-quotation`, `download-quotation-pdf` (`original`, `staff`, or `final`) |
90
- | **Approvals** | `list-approvals` / `get-pending-approvals` (`--type budget\|supplier\|bill\|quotation\|all`), `approve-bill` / `reject-bill` (send reply email), `approve-budget` / `reject-budget` (send reply email), `approve-supplier` / `reject-supplier` (send reply email), `approve-quotation` / `reject-quotation` |
91
- | **Companies & projects** | `list-companies`, `get-company`, `create-company`, `update-company` (`--payload`), `delete-company`, `list-projects`, `get-project` (its budget overview returns `totalRevenue`, calculated only from Estimate Accepted and Estimate Closed budgets), `create-project` (required: `--name`, `--companyId`, `--contactPersonId`, `--insideSalesId`, `--businessDevelopmentId`, `--venue`, `--startDate` as ISO datetime for project/window start; when `--asanaTaskId` is omitted, the CLI searches open Asana lead tasks in the Deals project, prompts for one of the top five matches, and resolves Slack channel fields from the selected deal card; optional `--asanaSearch`, `--pax`, `--endDate`, `--description`, `--projectManagerId`; always requests QBO project import like the web app), `update-project` (`--payload` with `dateRange.from` / `dateRange.to` for the project window; optional `projectManagerId` and `requestQboAccountantNotification` in JSON), `delete-project`, `update-project-status` (`<id>` `<status>`: `PITCH` \| `WON` \| `COMPLETED` \| `RECONCILED` \| `LOST`; close-out follows `WON` → `RECONCILED` → `COMPLETED`, and reconciliation requires every accepted/closed budget line to have an Approved/Paid bill or claim or be explicitly Not Utilized; for `PITCH` → `WON` also pass a Budget Builder user `--projectManagerId` or `--projectManagerEmail`; when marking `WON` without an accepted/closed budget or proof, pass `--wonOverrideReason`) |
87
+ | **Budgets** | `list-budgets` (full payload by default; `--summary` or `--includeDetails false` for slim list), `get-budget`, `get-budget-items`, `get-budget-details`, `get-budget-categories`, `get-budget-versions`, `rename-budget-version`, `restore-budget-version`, `update-budget-status` (`ESTIMATE_ACCEPTED` requires win proof, auto-marks `PITCH` or `LOST` projects `WON`, and auto-creates an Asana Event Ops section from Prompt 5 tasks when enabled; Prompt 5 skips quantity-zero items, combines matching non-GO-internal suppliers, keeps each GO internal item separate, removes generated work that repeats another line item or group, and schedules applicable subtasks relative to the project start date; `--projectStatusOnCommercialRejection PITCH\|LOST` is required when rejecting the only accepted/closed budget on a commercial project), `mark-budget-won` (`<budgetId>` + proof file path; `PITCH` or `LOST` projects become `WON` automatically), `create-budget` / `update-budget` (`--payload`), `delete-budget`, `create-budget-approval` (also sends approval request emails), `add-budget-items`, `update-budget-item`, `remove-budget-item`, `reorder-budget-items`, `update-budget-item-supplier`, `mark-budget-items-not-utilized`, `restore-budget-item`, `create-placeholder-bill` (admin recovery), `create-budget-category`, `update-budget-category`, `delete-budget-category`, `update-budget-commission`, `delete-budget-commission`, `update-budget-discount`, `delete-budget-discount` (`--payload` where noted), `upload-budget-attachment` (`<budgetId>` + local file path; uses `attachment.requestBudgetAttachmentUpload` + PUT + `attachment.confirmBudgetAttachment`) |
88
+ | **Bills / claims** | `list-bills` (`--isClaimable false` for bills, `--isClaimable true` for claims, omit for both), `list-claims` (claims only), `validate-bill-selection` (`--payload`; runs the same supplier, line-item, and quotation-coverage checks as the web flow), `stage-bill-attachment` (securely uploads invoice/payment-proof files before creation and returns attachment JSON), `cleanup-staged-bill-attachments`, `create-bill` (`--payload`; set `isClaimable=false` for a bill, `isClaimable=true` for a claim; supplier bills require positive `extractedAmount` and `amount`, with `amount <= extractedAmount`; an already-paid supplier bill sets `alreadyPaid=true` and requires `paymentReference` plus a staged PDF in `paymentProofAttachments`, and is created pending finance approval; non-legacy supplier bills from 1 Jul 2026 00:00 SGT also require approved quotation coverage), `update-bill` (`--payload`), `update-bill-payment-evidence` (`--payload`; replaces the payment reference and payment-proof PDFs for an already-paid bill), `delete-bill`, `create-bill-approval` (also sends approval request emails), `update-bill-status` (moving a bill or claim to `PAID` sends creator and supplier emails, CC `m@getout.sg` and BCC `noah@getout.sg`; a claim sends one email when both recipient addresses match), `patch-bill-payment` (PAID bills: `--paymentTrackingUrl`, `--paymentReference`, `--quickbooksBillId`, `--paymentDate` ISO; clearing a paid bill's QuickBooks link is not allowed), `patch-bill-invoice-number`, `get-bill-attachments`, `upload-bill-attachment` (`<billId>` + local path), `get-bill-details` |
89
+ | **Quotations** | `list-quotations` (supports project, budget, supplier, status, requester, and text-search filters), `get-quotation`, `upload-quotation-attachment` (`<projectId>` + PDF/JPEG/PNG path up to 20MB; returns attachment JSON for payload use), `cleanup-staged-quotation-attachments`, `create-quotation` (`--payload` for `quotation.createDraft`; include `amountWithoutGst` and `gstAmount`), `update-quotation` (`--payload`; updates a `DRAFT` or `REJECTED` quotation), `delete-quotation`, `submit-quotation`, `approve-quotation`, `reject-quotation`, `download-quotation-pdf` (`original`, `staff`, or `final`) |
90
+ | **Customer invoices** | `check-customer-invoice-readiness`, `list-customer-invoices`, `get-customer-invoice`, `create-customer-invoice`, `discard-customer-invoice`, `delete-customer-invoice`, `void-customer-invoice`, `approve-customer-invoice`, `reject-customer-invoice`, `download-customer-invoice-pdf`, `sync-customer-invoice` |
91
+ | **Approvals** | `list-approvals` / `get-pending-approvals` (`--type budget\|supplier\|bill\|quotation\|customer_invoice\|all`), `approve-bill` / `reject-bill` (send reply email), `approve-budget` / `reject-budget` (send reply email), `approve-supplier` / `reject-supplier` (send reply email), `approve-quotation` / `reject-quotation`, `approve-customer-invoice` / `reject-customer-invoice` |
92
+ | **Companies & projects** | `list-companies`, `get-company`, `create-company`, `update-company` (`--payload`), `delete-company`, `list-projects`, `get-project` (its budget overview returns `totalRevenue`, calculated only from Estimate Accepted and Estimate Closed budgets), `create-project` (required: `--name`, `--companyId`, `--contactPersonId`, `--insideSalesId`, `--businessDevelopmentId`, `--venue`, `--startDate` as ISO datetime for project/window start; when `--asanaTaskId` is omitted, the CLI searches open Asana lead tasks in the Deals project, prompts for one of the top five matches, and resolves Slack channel fields from the selected deal card; optional `--asanaSearch`, `--pax`, `--endDate`, `--description`, `--projectManagerId`; always requests QBO project import like the web app), `update-project` (`--payload` with `dateRange.from` / `dateRange.to` for the project window; optional `projectManagerId` and `requestQboAccountantNotification` in JSON), `delete-project`, `check-project-reconciliation <id>` (runs the web app's live checks without changing status), `reconcile-project <id>` (reruns checks transactionally and marks an eligible project `RECONCILED`), `complete-project <id>` (reruns validation, marks a reconciled project `COMPLETED`, and queues QuickBooks placeholder cleanup), `import-qbo-project`, `update-project-status` (`<id>` `<status>`: `PITCH` \| `WON` \| `COMPLETED` \| `RECONCILED` \| `LOST`; close-out follows `WON` → `RECONCILED` → `COMPLETED`, and reconciliation requires every accepted/closed budget line to have an Approved/Paid bill or claim or be explicitly Not Utilized; marking `COMPLETED` queues deletion of every remaining project placeholder bill from QuickBooks; for `PITCH` → `WON` also pass a Budget Builder user `--projectManagerId` or `--projectManagerEmail`; when marking `WON` without an accepted/closed budget or proof, pass `--wonOverrideReason`) |
92
93
  | **Contacts** | `list-contacts`, `create-contact-person` (`--payload`), `update-contact-person` (`--payload`) |
93
94
  | **Suppliers & items** | `list-suppliers` (defaults to active suppliers, `--perPage 10`, sorted by `createdAt` desc; supports `--name`, `--sortBy` for scalar supplier fields, `--sortDir`, `--createdBy`, `--gstRegistered`, `--status`, `--supplierTags`, `--active false` for archived suppliers), `create-supplier` (`--payload`; when supplier status is `PENDING_APPROVAL`, also runs `supplier.createSupplierApproval` and `email.sendSupplierApprovalRequestEmail`), `update-supplier` (`--payload`; when supplier status is `PENDING_APPROVAL`, also runs `supplier.createSupplierApproval` and `email.sendSupplierApprovalRequestEmail`), `delete-suppliers` (`--ids` CSV; admin; archives/deactivates related items), `reactivate-suppliers` (`--ids` CSV; admin; reactivates related items), `create-certification` / `create-payment-method` / `create-supplier-role` / `create-supplier-tag` (`--name`), `get-supplier-details` (includes `supplierApprovalSummary` for pending approvers, superseded approvers, and the actual responder/respondedAt metadata), `get-supplier-analytics`, `list-items`, `create-item` (`--payload`), `update-item` (`--payload`), `delete-item`, `get-item`, `list-item-categories`, `create-item-category`, `update-item-category`, `delete-item-categories` (`--ids` CSV; admin) |
94
95
  | **Dashboard & users** | `list-users`, `create-user` (admin; provisions an API-only service identity with no Google sign-in; `--email`, optional `--name`, optional `--role` defaulting to `USER`), `create-api-key` (admin; `--userId`, `--name`; raw key shown once), `list-api-keys` (admin; optional `--userId`), `revoke-api-key` (admin; key ID plus `--userId` for another user's key), `get-user-performance`, `get-dashboard`, `get-monthly-metrics`, `get-system-overview`, `get-estimate-performance`, `get-financial-overview` |
95
96
  | **Errors** | `get-recent-errors`, `get-error-metrics` |
97
+ | **Automation (admin)** | `list-integration-operations`, `retry-integration-operation` |
96
98
  | **Historical / benchmarks** | `get-approved-budgets`, `get-budget-category-benchmarks`, `get-item-pricing-history`, `get-supplier-pricing-history` |
97
99
 
100
+ The CLI intentionally wraps low-level upload-request/confirm procedures into file-based commands and omits browser-only helpers such as navigation counts, combobox/facet data, recent-page bookkeeping, and live UI subscriptions. Operational workflows for bills, claims, quotations, customer invoices, project reconciliation/completion, and integration recovery are available directly.
101
+
98
102
  ## Output
99
103
 
100
104
  - **Stdout:** JSON (pretty-printed)
package/dist/commands.js CHANGED
@@ -1,18 +1,19 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { readFile } from "node:fs/promises";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
3
  import { basename } from "node:path";
4
4
  import { createInterface } from "node:readline/promises";
5
5
  import { contentType } from "mime-types";
6
6
  import { api } from "./api-client.js";
7
7
  import { BudgetRole, Deals, ExtendedApprovalStatus, ExtendedApprovalType, ExtendedBudgetStatus, TimeFrame, } from "./filter-enums.js";
8
8
  import { billStatusesForApi, } from "./parse-cli-enums.js";
9
- import { parseBudgetDiscountPayload, parseCompanyUpdatePayload, parseContactCreatePayload, parseContactUpdatePayload, parseCreateBillPayload, parseCreateBudgetPayload, parseItemCreatePayload, parseItemUpdatePayload, parseSupplierCreatePayload, parseSupplierUpdatePayload, parseUpdateBillPayload, parseUpdateBudgetCommissionPayload, parseUpdateBudgetPayload, parseUpdateProjectPayload, } from "./parse-mutation-payload.js";
10
- import { BudgetStatus, } from "./prisma-enums.js";
9
+ import { parseBudgetDiscountPayload, parseCompanyUpdatePayload, parseContactCreatePayload, parseContactUpdatePayload, parseCreateBillPayload, parseCreateBudgetPayload, parseCreateCustomerInvoicePayload, parseCreateQuotationPayload, parseItemCreatePayload, parseItemUpdatePayload, parseSupplierCreatePayload, parseSupplierUpdatePayload, parseUpdateBillPayload, parseUpdateBillPaymentEvidencePayload, parseUpdateBudgetCommissionPayload, parseUpdateBudgetPayload, parseUpdateProjectPayload, parseUpdateQuotationPayload, parseValidateBillSelectionPayload, } from "./parse-mutation-payload.js";
10
+ import { BudgetStatus, ProjectStatus, } from "./prisma-enums.js";
11
11
  import { createRichTextFromPlainText } from "./rich-text.js";
12
12
  const BUDGET = "BUDGET";
13
13
  const BILL = "BILL";
14
14
  const SUPPLIER = "SUPPLIER";
15
15
  const QUOTATION = "QUOTATION";
16
+ const CUSTOMER_INVOICE = "CUSTOMER_INVOICE";
16
17
  const ASANA_WON_LOST_SECTION_GIDS = new Set([
17
18
  "1211678338364908",
18
19
  "1211678338364907",
@@ -261,6 +262,16 @@ const billAttachmentContentTypeForFileName = (fileName) => {
261
262
  }
262
263
  return resolvedContentType;
263
264
  };
265
+ const QUOTATION_ATTACHMENT_MAX_SIZE = 20 * 1024 * 1024;
266
+ const quotationAttachmentContentTypeForFileName = (fileName) => {
267
+ const resolvedContentType = contentTypeHeaderForFileName(fileName);
268
+ if (resolvedContentType === "application/pdf" ||
269
+ resolvedContentType === "image/jpeg" ||
270
+ resolvedContentType === "image/png") {
271
+ return resolvedContentType;
272
+ }
273
+ throw new Error(`Unsupported quotation attachment type: ${fileName}. Use PDF, JPEG, or PNG.`);
274
+ };
264
275
  const LOCKED_BUDGET_STATUSES_REQUIRING_CONFIRMATION = new Set([
265
276
  BudgetStatus.ESTIMATE_CREATED,
266
277
  BudgetStatus.ESTIMATE_SENT,
@@ -533,6 +544,10 @@ export async function listApprovals(opts) {
533
544
  id = n.supplierQuotation.id;
534
545
  name = `Quotation ${n.supplierQuotation.supplier.name}`;
535
546
  }
547
+ else if (n.type === CUSTOMER_INVOICE && n.customerInvoiceBatch) {
548
+ id = n.customerInvoiceBatch.id;
549
+ name = `Customer invoice ${n.customerInvoiceBatch.project.name}`;
550
+ }
536
551
  else {
537
552
  id = n.id;
538
553
  name = "Unknown";
@@ -668,6 +683,13 @@ export async function stageBillAttachmentsFromPaths(projectId, filePaths) {
668
683
  attachments,
669
684
  });
670
685
  }
686
+ export const cleanupStagedBillAttachments = async (projectId, keys) => {
687
+ const result = await api.attachment.deleteStagedBillAttachments.mutate({
688
+ projectId,
689
+ keys,
690
+ });
691
+ out(result);
692
+ };
671
693
  export async function uploadBillAttachmentFromPath(billId, filePath, type = "BILL") {
672
694
  const confirmed = await uploadSingleBillAttachmentFromPath(billId, filePath, type);
673
695
  out(confirmed);
@@ -740,6 +762,10 @@ export async function uploadBillDocumentsFromPaths({ billId, invoicePaths = [],
740
762
  export async function uploadQuotationAttachmentFromPath(projectId, filePath) {
741
763
  const buf = await readFile(filePath);
742
764
  const fileName = basename(filePath);
765
+ if (buf.byteLength > QUOTATION_ATTACHMENT_MAX_SIZE) {
766
+ throw new Error("Quotation attachment must be 20MB or smaller.");
767
+ }
768
+ const attachmentContentType = quotationAttachmentContentTypeForFileName(fileName);
743
769
  const ext = fileName
744
770
  .split(".")
745
771
  .pop()
@@ -752,7 +778,7 @@ export async function uploadQuotationAttachmentFromPath(projectId, filePath) {
752
778
  const res = await fetch(uploadUrl, {
753
779
  method: "PUT",
754
780
  body: buf,
755
- headers: { "Content-Type": contentTypeHeaderForFileName(fileName) },
781
+ headers: { "Content-Type": attachmentContentType },
756
782
  });
757
783
  if (!res.ok) {
758
784
  throw new Error(`S3 upload failed: HTTP ${res.status} ${(await res.text()).slice(0, 500)}`);
@@ -764,6 +790,13 @@ export async function uploadQuotationAttachmentFromPath(projectId, filePath) {
764
790
  size: buf.byteLength,
765
791
  });
766
792
  }
793
+ export const cleanupStagedQuotationAttachments = async (projectId, keys) => {
794
+ const result = await api.quotation.deleteStagedAttachments.mutate({
795
+ projectId,
796
+ keys,
797
+ });
798
+ out(result);
799
+ };
767
800
  export async function uploadBudgetAttachmentFromPath(budgetId, filePath) {
768
801
  await confirmLockedBudgetChangeByBudgetId(budgetId, "Upload budget attachment");
769
802
  const buf = await readFile(filePath);
@@ -990,6 +1023,68 @@ export async function updateBudgetDiscountFromPayload(raw) {
990
1023
  const result = await api.budget.updateBudgetDiscount.mutate(input);
991
1024
  out(result);
992
1025
  }
1026
+ export const deleteBudgetDiscount = async (budgetId) => {
1027
+ await assertSensitiveWorkflowConfirmed({
1028
+ action: "Remove budget discount",
1029
+ entity: `budget ${budgetId}`,
1030
+ details: "and return the budget to DRAFT",
1031
+ });
1032
+ const result = await api.budget.deleteBudgetDiscount.mutate({ budgetId });
1033
+ out(result);
1034
+ };
1035
+ export const deleteBudgetCommission = async (budgetId) => {
1036
+ await assertSensitiveWorkflowConfirmed({
1037
+ action: "Remove budget commission",
1038
+ entity: `budget ${budgetId}`,
1039
+ details: "and return the budget to DRAFT",
1040
+ });
1041
+ const result = await api.budget.deleteBudgetCommission.mutate({ budgetId });
1042
+ out(result);
1043
+ };
1044
+ export const setBudgetItemsNotUtilized = async (budgetItemIds, notUtilized) => {
1045
+ await assertSensitiveWorkflowConfirmed({
1046
+ action: notUtilized
1047
+ ? "Mark budget lines Not Utilized"
1048
+ : "Restore budget line",
1049
+ entity: `budget item${budgetItemIds.length === 1 ? "" : "s"} ${budgetItemIds.join(", ")}`,
1050
+ details: notUtilized
1051
+ ? "and remove linked QuickBooks placeholder expenses"
1052
+ : "and restore its QuickBooks placeholder expense when required",
1053
+ });
1054
+ const result = await api.budget.setBudgetItemsNotUtilized.mutate({
1055
+ budgetItemIds,
1056
+ notUtilized,
1057
+ });
1058
+ out(result);
1059
+ };
1060
+ export const createPlaceholderBillForBudgetItem = async (budgetItemId) => {
1061
+ await assertSensitiveWorkflowConfirmed({
1062
+ action: "Create QuickBooks placeholder bill",
1063
+ entity: `budget item ${budgetItemId}`,
1064
+ });
1065
+ const result = await api.budget.createPlaceholderBillForBudgetItem.mutate({
1066
+ budgetItemId,
1067
+ });
1068
+ out(result);
1069
+ };
1070
+ export const renameBudgetVersion = async (versionId, newName) => {
1071
+ const result = await api.budgetVersion.renameBudgetVersion.mutate({
1072
+ versionId,
1073
+ newName,
1074
+ });
1075
+ out(result);
1076
+ };
1077
+ export const restoreBudgetVersion = async (versionId) => {
1078
+ await assertSensitiveWorkflowConfirmed({
1079
+ action: "Restore budget version",
1080
+ entity: `budget version ${versionId}`,
1081
+ details: "replace the current budget, project fields, items, and attachments, clear its QuickBooks estimate link, and return it to DRAFT",
1082
+ });
1083
+ const result = await api.budgetVersion.restoreBudgetVersion.mutate({
1084
+ versionId,
1085
+ });
1086
+ out(result);
1087
+ };
993
1088
  export async function deleteItemById(itemId) {
994
1089
  const result = await api.item.deleteItem.mutate({ id: itemId });
995
1090
  out(result);
@@ -1044,9 +1139,19 @@ export async function getQuotationDetails(id) {
1044
1139
  out(result);
1045
1140
  }
1046
1141
  export async function createQuotationFromPayload(raw) {
1047
- const result = await api.quotation.createDraft.mutate(raw);
1142
+ const input = parseCreateQuotationPayload(raw);
1143
+ const result = await api.quotation.createDraft.mutate(input);
1048
1144
  out(result);
1049
1145
  }
1146
+ export const updateQuotationFromPayload = async (raw) => {
1147
+ const input = parseUpdateQuotationPayload(raw);
1148
+ const result = await api.quotation.updateDraftOrRejected.mutate(input);
1149
+ out(result);
1150
+ };
1151
+ export const deleteQuotationById = async (id) => {
1152
+ const result = await api.quotation.delete.mutate({ id });
1153
+ out(result);
1154
+ };
1050
1155
  export async function submitQuotation(id) {
1051
1156
  await assertSensitiveWorkflowConfirmed({
1052
1157
  action: "Submit quotation for approval",
@@ -1062,11 +1167,121 @@ export async function downloadQuotationPdf(id, fileRole) {
1062
1167
  });
1063
1168
  out(result);
1064
1169
  }
1170
+ export const checkCustomerInvoiceReadiness = async (budgetId) => {
1171
+ const result = await api.customerInvoice.getInvoiceReadiness.query({
1172
+ budgetId,
1173
+ });
1174
+ out(result);
1175
+ };
1176
+ export const listCustomerInvoices = async (input) => {
1177
+ const result = await api.customerInvoice.listByBudget.query(input);
1178
+ out(result);
1179
+ };
1180
+ export const getCustomerInvoice = async (batchId) => {
1181
+ const result = await api.customerInvoice.getInvoiceDetail.query({ batchId });
1182
+ out(result);
1183
+ };
1184
+ export const createCustomerInvoice = async (raw) => {
1185
+ const input = parseCreateCustomerInvoicePayload(raw);
1186
+ await assertSensitiveWorkflowConfirmed({
1187
+ action: "Create customer invoice",
1188
+ entity: `budget ${input.budgetId}`,
1189
+ details: "create the invoice in QuickBooks and either approve it as admin or send approval-request emails",
1190
+ });
1191
+ const result = await api.customerInvoice.createInvoiceBatch.mutate(input);
1192
+ out(result);
1193
+ };
1194
+ export const discardCreatingCustomerInvoice = async (batchId) => {
1195
+ await assertSensitiveWorkflowConfirmed({
1196
+ action: "Discard unfinished customer invoice",
1197
+ entity: `invoice batch ${batchId}`,
1198
+ });
1199
+ const result = await api.customerInvoice.discardCreatingInvoiceBatch.mutate({
1200
+ batchId,
1201
+ });
1202
+ out(result);
1203
+ };
1204
+ export const deleteCustomerInvoice = async (batchId) => {
1205
+ await assertSensitiveWorkflowConfirmed({
1206
+ action: "Delete customer invoice",
1207
+ entity: `invoice batch ${batchId}`,
1208
+ details: "delete its invoices from QuickBooks and remove the local batch",
1209
+ });
1210
+ const result = await api.customerInvoice.deleteInvoiceBatch.mutate({
1211
+ batchId,
1212
+ });
1213
+ out(result);
1214
+ };
1215
+ export const voidCustomerInvoice = async (batchId) => {
1216
+ await assertSensitiveWorkflowConfirmed({
1217
+ action: "Void customer invoice",
1218
+ entity: `invoice batch ${batchId}`,
1219
+ details: "void its invoices in QuickBooks",
1220
+ });
1221
+ const result = await api.customerInvoice.voidInvoiceBatch.mutate({ batchId });
1222
+ out(result);
1223
+ };
1224
+ export const approveCustomerInvoice = async (batchId) => {
1225
+ await assertSensitiveWorkflowConfirmed({
1226
+ action: "Approve customer invoice",
1227
+ entity: `invoice batch ${batchId}`,
1228
+ details: "and notify its creator",
1229
+ });
1230
+ const result = await api.customerInvoice.approveInvoiceBatch.mutate({
1231
+ batchId,
1232
+ });
1233
+ out(result);
1234
+ };
1235
+ export const rejectCustomerInvoice = async (batchId, rejectionReason) => {
1236
+ await assertSensitiveWorkflowConfirmed({
1237
+ action: "Reject customer invoice",
1238
+ entity: `invoice batch ${batchId}`,
1239
+ details: `with reason "${rejectionReason}", void it in QuickBooks, and notify its creator`,
1240
+ });
1241
+ const result = await api.customerInvoice.rejectInvoiceBatch.mutate({
1242
+ batchId,
1243
+ rejectionReason,
1244
+ });
1245
+ out(result);
1246
+ };
1247
+ export const downloadCustomerInvoicePdf = async (invoiceId, outputPath) => {
1248
+ const result = await api.customerInvoice.downloadPdf.mutate({ invoiceId });
1249
+ const resolvedOutputPath = outputPath?.trim() || result.fileName;
1250
+ const contents = Buffer.from(result.base64, "base64");
1251
+ await writeFile(resolvedOutputPath, contents);
1252
+ out({
1253
+ contentType: result.contentType,
1254
+ fileName: result.fileName,
1255
+ outputPath: resolvedOutputPath,
1256
+ size: contents.byteLength,
1257
+ });
1258
+ };
1259
+ export const syncCustomerInvoice = async (invoiceId) => {
1260
+ await assertSensitiveWorkflowConfirmed({
1261
+ action: "Sync customer invoice from QuickBooks",
1262
+ entity: `invoice ${invoiceId}`,
1263
+ details: "and update its local status, QuickBooks metadata, balance, and history",
1264
+ });
1265
+ const result = await api.customerInvoice.syncInvoiceStatus.mutate({
1266
+ invoiceId,
1267
+ });
1268
+ out(result);
1269
+ };
1065
1270
  export async function updateBillFromPayload(raw) {
1066
1271
  const input = parseUpdateBillPayload(raw);
1067
1272
  const result = await api.bill.update.mutate(input);
1068
1273
  out(result);
1069
1274
  }
1275
+ export const validateBillSelectionFromPayload = async (raw) => {
1276
+ const input = parseValidateBillSelectionPayload(raw);
1277
+ const result = await api.bill.validateSelection.mutate(input);
1278
+ out(result);
1279
+ };
1280
+ export const updateBillPaymentEvidenceFromPayload = async (raw) => {
1281
+ const input = parseUpdateBillPaymentEvidencePayload(raw);
1282
+ const result = await api.bill.updatePaymentEvidence.mutate(input);
1283
+ out(result);
1284
+ };
1070
1285
  export async function deleteBillById(id) {
1071
1286
  const result = await api.bill.delete.mutate({ id });
1072
1287
  out(result);
@@ -1576,6 +1791,47 @@ export async function updateProjectStatus(id, status, opts) {
1576
1791
  });
1577
1792
  out(result);
1578
1793
  }
1794
+ export const checkProjectReconciliation = async (projectId) => {
1795
+ const result = await api.project.getReconciliationReview.query({ projectId });
1796
+ out(result);
1797
+ };
1798
+ export const reconcileProject = async (projectId) => {
1799
+ await assertSensitiveWorkflowConfirmed({
1800
+ action: "Reconcile project",
1801
+ entity: `project ${projectId}`,
1802
+ details: "rerun all reconciliation checks and mark it RECONCILED",
1803
+ });
1804
+ const result = await api.project.reconcile.mutate({ projectId });
1805
+ out(result);
1806
+ };
1807
+ export const completeProject = async (projectId) => {
1808
+ await updateProjectStatus(projectId, ProjectStatus.COMPLETED);
1809
+ };
1810
+ export const importQuickBooksProjectId = async (projectId, projectUrl) => {
1811
+ const result = await api.project.importQboProjectId.mutate({
1812
+ projectId,
1813
+ projectUrl,
1814
+ });
1815
+ out(result);
1816
+ };
1817
+ export const listIntegrationOperations = async (input) => {
1818
+ const result = await api.integration.listOperations.query(input);
1819
+ out(result);
1820
+ };
1821
+ export const retryIntegrationOperation = async (operationId, confirmExternalStateReconciled) => {
1822
+ await assertSensitiveWorkflowConfirmed({
1823
+ action: "Retry integration operation",
1824
+ entity: `operation ${operationId}`,
1825
+ details: confirmExternalStateReconciled
1826
+ ? "after confirming the external state has been reconciled"
1827
+ : undefined,
1828
+ });
1829
+ const result = await api.integration.retryOperation.mutate({
1830
+ operationId,
1831
+ confirmExternalStateReconciled,
1832
+ });
1833
+ out(result);
1834
+ };
1579
1835
  // --- Contacts ---
1580
1836
  export async function listContacts(companyId) {
1581
1837
  const contacts = await api.contactPerson.getContactPersonByCompanyId.query({
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, 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.
@@ -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
@@ -163,8 +200,11 @@ Bills
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
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.
166
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.
167
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.
168
208
  delete-bill <billId>
169
209
  update-bill-status <id> <status> [--rejectionReason] [--paymentTrackingUrl] [--paymentReference]
170
210
  status (${billStatusesForUpdateHelp.join(", ")}) — not PENDING_APPROVAL; use create-bill-approval
@@ -179,17 +219,33 @@ Bills
179
219
  get-bill-details <billId>
180
220
 
181
221
  Quotations
182
- list-quotations [--projectId] [--budgetId] [--supplierId] [--status CSV] [--page] [--pageSize]
222
+ list-quotations [--projectId] [--budgetId] [--supplierId] [--status CSV] [--requesterIds CSV] [--search] [--page] [--pageSize]
183
223
  get-quotation <quotationId>
184
- upload-quotation-attachment <projectId> <filePath> Presigned S3 upload; returns attachment JSON for create-quotation payload
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.
185
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>
186
229
  submit-quotation <quotationId>
187
230
  approve-quotation <quotationId>
188
231
  reject-quotation <quotationId> --reason <text>
189
232
  download-quotation-pdf <quotationId> [original|staff|final]
190
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
+
191
247
  Approvals
192
- 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]
193
249
  approve-bill <billId> (also sends requester reply email)
194
250
  reject-bill <billId> --reason <text> (also sends requester reply email)
195
251
  approve-budget <budgetId> (also sends requester reply email)
@@ -211,8 +267,12 @@ Companies & projects
211
267
  create-project --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --startDate <ISO> [--projectManagerId <userId>] [--asanaTaskId] [--asanaSearch <text>] [--slackChannelId --slackChannelUrl --slackChannelName] [--pax] [--endDate <ISO>] [--description]
212
268
  update-project --payload '<json>' (project.updateProject; optional projectManagerId and requestQboAccountantNotification; default notify like web)
213
269
  delete-project <id>
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>
214
274
  update-project-status <id> <status> [--projectManagerId <userId>] [--projectManagerEmail <email>] [--wonOverrideReason <text>]
215
- Required when moving PITCH → WON: supply --projectManagerId or --projectManagerEmail.
275
+ Required when moving PITCH → WON: supply --projectManagerId or --projectManagerEmail.
216
276
  Required when moving to WON without accepted/closed budget or win proof: --wonOverrideReason (10+ chars).
217
277
  status: ${projectStatusesForHelp.join(", ")}
218
278
 
@@ -261,6 +321,10 @@ Errors
261
321
  get-recent-errors [--page] [--perPage] [--severity] [--status]
262
322
  get-error-metrics
263
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
+
264
328
  Historical / benchmarks
265
329
  get-approved-budgets [--projectId] [--limit] [--minRevenue] [--maxRevenue]
266
330
  get-budget-category-benchmarks <categoryId> [--limit]
@@ -377,6 +441,23 @@ async function main() {
377
441
  await getBudgetVersions(id);
378
442
  break;
379
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
+ }
380
461
  case "update-budget-status": {
381
462
  const [id, status] = positional;
382
463
  if (!id || !status) {
@@ -560,6 +641,46 @@ async function main() {
560
641
  await updateBudgetDiscountFromPayload(raw);
561
642
  break;
562
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
+ }
563
684
  case "list-bills": {
564
685
  await listBills(parseListBillsFlags(flags));
565
686
  break;
@@ -587,6 +708,15 @@ async function main() {
587
708
  await createBillFromPayload(raw);
588
709
  break;
589
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
+ }
590
720
  case "update-bill": {
591
721
  const payloadRaw = getFlag(flags, "payload");
592
722
  if (!payloadRaw) {
@@ -596,6 +726,15 @@ async function main() {
596
726
  await updateBillFromPayload(raw);
597
727
  break;
598
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
+ }
599
738
  case "delete-bill": {
600
739
  const id = positional[0];
601
740
  if (!id)
@@ -714,6 +853,15 @@ async function main() {
714
853
  await stageBillAttachmentsFromPaths(projectId, filePaths);
715
854
  break;
716
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
+ }
717
865
  case "upload-bill-attachment":
718
866
  case "attach-bill-attachment":
719
867
  case "attach-bill-payment-receipt": {
@@ -794,12 +942,9 @@ async function main() {
794
942
  projectId: getFlag(flags, "projectId"),
795
943
  budgetId: getFlag(flags, "budgetId"),
796
944
  supplierId: getFlag(flags, "supplierId"),
797
- status: statusRaw !== undefined
798
- ? String(statusRaw)
799
- .split(",")
800
- .map((value) => value.trim().toUpperCase())
801
- .filter(Boolean)
802
- : undefined,
945
+ status: parseCommaSeparatedQuotationStatuses(statusRaw),
946
+ requesterIds: parseCommaSeparatedIds(getFlag(flags, "requesterIds")),
947
+ search: getFlag(flags, "search"),
803
948
  page: getFlag(flags, "page") !== undefined
804
949
  ? Number(getFlag(flags, "page"))
805
950
  : undefined,
@@ -824,6 +969,15 @@ async function main() {
824
969
  await uploadQuotationAttachmentFromPath(projectId, filePath);
825
970
  break;
826
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
+ }
827
981
  case "create-quotation": {
828
982
  const payloadRaw = getFlag(flags, "payload");
829
983
  if (!payloadRaw) {
@@ -833,6 +987,22 @@ async function main() {
833
987
  await createQuotationFromPayload(raw);
834
988
  break;
835
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
+ }
836
1006
  case "submit-quotation": {
837
1007
  const id = positional[0];
838
1008
  if (!id)
@@ -866,10 +1036,114 @@ async function main() {
866
1036
  ? "ORIGINAL"
867
1037
  : roleRaw === "staff"
868
1038
  ? "STAFF_SIGNED"
869
- : "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
+ }
870
1045
  await downloadQuotationPdf(id, fileRole);
871
1046
  break;
872
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
+ }
873
1147
  case "list-approvals":
874
1148
  case "get-pending-approvals": {
875
1149
  const typeRaw = getFlag(flags, "type");
@@ -1057,6 +1331,37 @@ async function main() {
1057
1331
  await deleteProjectById(id);
1058
1332
  break;
1059
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
+ }
1060
1365
  case "update-project-status": {
1061
1366
  const [id, statusRaw] = positional;
1062
1367
  if (!id || !statusRaw) {
@@ -1344,6 +1649,24 @@ async function main() {
1344
1649
  await getErrorMetrics();
1345
1650
  break;
1346
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
+ }
1347
1670
  case "get-approved-budgets": {
1348
1671
  await getApprovedBudgets({
1349
1672
  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
  }
@@ -190,6 +190,42 @@ export function parseUpdateBillPayload(raw) {
190
190
  id: requiredString(o.id, "update-bill payload.id"),
191
191
  };
192
192
  }
193
+ /** Server performs the authoritative bill-selection and quotation-coverage checks. */
194
+ export function parseValidateBillSelectionPayload(raw) {
195
+ requireObject(raw, "validate-bill-selection payload");
196
+ return raw;
197
+ }
198
+ /** Matches bill.updatePaymentEvidence for already-paid supplier bills. */
199
+ export function parseUpdateBillPaymentEvidencePayload(raw) {
200
+ requireObject(raw, "update-bill-payment-evidence payload");
201
+ return raw;
202
+ }
203
+ /** Server validates quotation amounts, line items, and the single attachment. */
204
+ export function parseCreateQuotationPayload(raw) {
205
+ requireObject(raw, "create-quotation payload");
206
+ return raw;
207
+ }
208
+ export function parseUpdateQuotationPayload(raw) {
209
+ requireObject(raw, "update-quotation payload");
210
+ return raw;
211
+ }
212
+ export function parseCreateCustomerInvoicePayload(raw) {
213
+ const input = requireObject(raw, "create-customer-invoice payload");
214
+ if (!Array.isArray(input.splits) || input.splits.length === 0) {
215
+ throw new Error("create-customer-invoice payload.splits must be a non-empty array.");
216
+ }
217
+ return {
218
+ budgetId: requiredString(input.budgetId, "create-customer-invoice payload.budgetId"),
219
+ splits: input.splits.map((rawSplit, index) => {
220
+ const split = requireObject(rawSplit, `create-customer-invoice payload.splits[${index}]`);
221
+ return {
222
+ dueDate: toDate(split.dueDate, `create-customer-invoice payload.splits[${index}].dueDate`),
223
+ label: requiredString(split.label, `create-customer-invoice payload.splits[${index}].label`),
224
+ percentage: Number(split.percentage),
225
+ };
226
+ }),
227
+ };
228
+ }
193
229
  /** Matches project.updateProject — dateRange.from/to may be ISO strings. */
194
230
  export function parseUpdateProjectPayload(raw) {
195
231
  const o = requireObject(raw, "update-project payload");
@@ -10,8 +10,24 @@ export const ProjectStatus = {
10
10
  PITCH: "PITCH",
11
11
  WON: "WON",
12
12
  COMPLETED: "COMPLETED",
13
+ RECONCILED: "RECONCILED",
13
14
  LOST: "LOST",
14
15
  };
16
+ export const SupplierQuotationStatus = {
17
+ DRAFT: "DRAFT",
18
+ PENDING_APPROVAL: "PENDING_APPROVAL",
19
+ APPROVED: "APPROVED",
20
+ REJECTED: "REJECTED",
21
+ SUPERSEDED: "SUPERSEDED",
22
+ };
23
+ export const CustomerInvoiceBatchStatus = {
24
+ CREATING: "CREATING",
25
+ PENDING_APPROVAL: "PENDING_APPROVAL",
26
+ APPROVED: "APPROVED",
27
+ REJECTED_VOIDED: "REJECTED_VOIDED",
28
+ EXPIRED_VOIDED: "EXPIRED_VOIDED",
29
+ PARTIAL_QBO_FAILURE: "PARTIAL_QBO_FAILURE",
30
+ };
15
31
  export const BudgetStatus = {
16
32
  DRAFT: "DRAFT",
17
33
  PENDING_APPROVAL: "PENDING_APPROVAL",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@go-labs-sg/bb",
3
- "version": "1.18.1",
3
+ "version": "1.19.0",
4
4
  "description": "Budget Builder CLI for AI agents — manage budgets, bills, and claims; bill records use isClaimable=false for bills and isClaimable=true for claims.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",