@go-labs-sg/bb 3.1.0 → 3.3.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
@@ -61,7 +61,7 @@ Roll out the API and maintained CLI together: `suppliers`/`pageCount` and offset
61
61
 
62
62
  **Error-log read shapes:** `bb error recent list` / `bb get-recent-errors` items now carry only the table columns: `id, severity, status, message, errorName, errorType, component, route, action, createdAt` and `user { id, name, email }`. Stack, cause, request details, metadata, tags, AI summary and resolution moved to the new `bb error get <errorId>` / `bb get-error <errorId>` (`errorLog.getErrorById`), which no longer returns alert-group bookkeeping or the raw `userId`/`resolvedById` columns (use `user`/`resolvedByUser`).
63
63
 
64
- **Company read shapes:** `company.getCompanyById` / `bb get-company` now return the company's own fields (`id, name, website, active, quickbooksId, createdById, createdAt, updatedAt`) without the embedded full `projects` array; use `bb list-projects --companyId <id>` for its projects. `bb list-companies` rows carry only `id, name, website, active, createdById, createdAt, updatedAt`, `createdBy { id, name, email, image }` and `_count`; use `bb get-company` for `quickbooksId`. Recent companies and the company QuickBooks sync status read only the fields they use; their outputs are unchanged.
64
+ **Company read shapes:** `company.getCompanyById` / `bb get-company` now return the company's own fields (`id, name, website, billingEmails, active, quickbooksId, createdById, createdAt, updatedAt`) without the embedded full `projects` array; use `bb list-projects --companyId <id>` for its projects. `bb list-companies` rows carry only `id, name, website, billingEmails, active, createdById, createdAt, updatedAt`, `createdBy { id, name, email, image }` and `_count`; use `bb get-company` for `quickbooksId`. Recent companies and the company QuickBooks sync status read only the fields they use; their outputs are unchanged.
65
65
 
66
66
  **Customer-invoice readiness and quotation list loading:** `customerInvoice.getInvoiceReadiness`, `getInvoiceAmountPreview`, `getEligibleBudgets` and `getProjectInvoiceReadiness` read the budget through one selected shape (only the budget, project, contact, invoice-settings, discount and item fields the readiness and amount helpers use) with Prisma's default join instead of forced per-relation queries. The quotation list page and metrics reads also use the default join. Their outputs are unchanged.
67
67
 
@@ -134,6 +134,14 @@ Cursors are opaque and scoped to the active account, filters, sort, and page wid
134
134
 
135
135
  Deploy the matching API before publishing the CLI. Cursor browsing is not a frozen export snapshot; restart without the cursor when the API reports an expired or mismatched boundary.
136
136
 
137
+ ## Company billing/AP invoice copies
138
+
139
+ Company detail and list reads include `billingEmails`. Set them for a new company with `bb company create --name "Example Co" --website "https://example.com" --billingEmails '["ap@example.com"]'`; existing companies reused by create retain their saved defaults. Use `bb company update --payload` with the current `id`, `name`, `website` and optional `billingEmails` to edit an existing company. Omit the field to preserve it or supply `[]` to clear it. The API validates up to 50 addresses, trims/lowercases and de-duplicates them. These state-changing commands retain their existing confirmation and external-write gates; billing emails themselves stay in BB.
140
+
141
+ `bb customer-invoice email-context get <batchId>` returns removable `billingCcEmails` separately from mandatory `requiredCcEmails`. When sending with `bb customer-invoice send --payload`, omit `cc` to use company defaults; an explicit list replaces those defaults and `[]` opts out. Required internal copies are still added. The queued outgoing-email record freezes the selected recipients for delivery/retry. Quotes/estimates and the primary To selection are unchanged; QuickBooks customer emails are not imported.
142
+
143
+ Apply the additive company schema before deploying the matching API/web and publishing this CLI. Production JTP Holdings acceptance remains pending the approved release; see the [billing/AP rollout and acceptance runbook](../../../docs/runbooks/budget-builder-company-billing-emails.md).
144
+
137
145
  ## Company discovery and reuse
138
146
 
139
147
  `bb company search --query "a star" [--page 2]` searches stored BB companies and QuickBooks company customers, including inactive records. Results identify `bb`, `qbo`, or `both`; punctuation/spacing normalization is for suggestions only. Search does not write to either system.
@@ -698,7 +706,7 @@ Admin/Lead users can run `bb supplier approve <supplierId>` to directly re-appro
698
706
  | **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` (forward cursor pages of active categories, name A–Z: `--q` / `--limit` / `--cursor`; returns `{ items, pageInfo }`), `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, adds 5 mandatory supplier subtasks ending in "Get bill and upload to Budget Builder", and sets no subtask due dates; `--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`; cloned unavailable lines require `unavailableItemReviewAcknowledged: true`), `delete-budget`, `create-budget-approval` (also sends approval request emails), `create-estimate`, `send-estimate-to-contact-person` (same contact-person email workflow as web), `add-budget-items`, `update-budget-item`, `replace-budget-item`, `approve-unavailable-item-exception` (Lead/Admin), `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`) |
699
707
  | **Bills / claims** | `list-bills` (`--isClaimable false` for bills, `--isClaimable true` for claims, omit for both), `list-claims` (claims only), `validate-bill-selection` (`--payload`, plus `--quotationIds q1,q2` or legacy `--quotationId q1`; validates the approved supplier and quotation coverage), `link-bill-quotations` (`<billId> --quotationIds q1,q2`; Admin source-link repair for eligible supplier bills), `stage-bill-attachment` (securely uploads invoice/payment-proof files before creation and returns attachment JSON; ownership and one-hour expiry are enforced by a server-side staged-upload record rather than encoded in the object key), `cleanup-staged-bill-attachments`, `create-bill` (`--payload`; set `isClaimable=false` for a bill and provide approved `quotationIds`, or use `--quotation-exempt` for an eligible server-validated exemption; set `isClaimable=true` for a claim without quotations; supplier bills require positive **before-GST** `extractedAmount` (full invoice subtotal) and `amount` (portion billed), with `amount <= extractedAmount`; line and quotation allocation amounts also exclude GST; an already-paid supplier bill sets `alreadyPaid=true` and requires `paymentReference` plus a staged PDF in `paymentProofAttachments`; admin creation automatically queues QBO finalization while other roles remain pending approval), `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` (`PAID` requires `--paymentReference`; pass `--paymentProof <receipt.pdf>` to stage and submit a PDF up to 20MB atomically, or omit it only when BB already has payment proof; moving to `PAID` runs the server's paid-bill notification workflow), `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` (newest-first cursor pages: `--limit` 1–50, `--cursor`; returns `{ items, pageInfo }`), `upload-bill-attachment` (`<billId>` + local path), `get-bill-details` (the web bill summary: status, amounts, people, payment, line totals), `get-bill-line-items` / `get-bill-approvals` (newest-first cursor pages: `--limit`, `--cursor`; `{ items, pageInfo }`), `get-bill-payment` (payment status, reference, date, tracking URL, QuickBooks link, external payment) |
700
708
  | **Quotations** | `list-quotations` (project, budget, supplier, status, requester and text-search filters; newest-first cursor pages via `--limit` / `--cursor` / `--direction` returning `{ items, pageInfo }`; `--page` / `--perPage` / `--pageSize` are rejected), `count-quotations` (`quotation.getCounts`: filters only, returns `totalCount`, status facets that ignore their own status filter, and `--includeMetrics true` amount metrics), `get-quotation` (web quotation shape), `get-quotation-line-items` (`<quotationId>`; newest-first cursor pages, `--limit 1-100`, `--cursor`), `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`, `gstAmount`, and `lineAmounts` entries `{ budgetItemId, amount }` for multiple selected items), `update-quotation` (`--payload`; updates a `DRAFT` or `REJECTED` quotation), `delete-quotation`, `submit-quotation`, `approve-quotation`, `reject-quotation`, `download-quotation-pdf` (`original`, `staff`, or `final`) |
701
- | **Customer invoices** | `check-customer-invoice-readiness`, `list-eligible-customer-invoice-budgets`, `preview-customer-invoice-amount`, `list-available-qbo-customer-invoices`, `list-customer-invoices` (global/project/budget filters; cursor pages via `--limit` / `--cursor` / `--direction` returning `{ items, pageInfo }`, `--page` / `--perPage` rejected; `--sortBy totalInvoiceAmount` sorts provider-confirmed totals including GST, with unknown amounts last), `count-customer-invoices` (`customerInvoice.getCounts`: filters only; `totalCount`, lifecycle status facets that ignore their own status filter, and amount metrics), `get-customer-invoice` (web-shaped batch), `get-customer-invoice-invoices`, `get-customer-invoice-lines`, `get-customer-invoice-approvals`, `get-customer-invoice-history`, `get-customer-invoice-email-context`, `preview-qbo-customer-invoice`, `import-qbo-customer-invoice` (BB-only link with reviewed token), `create-customer-invoice`, `discard-customer-invoice`, `delete-customer-invoice`, `void-customer-invoice`, `approve-customer-invoice` (admin), `reject-customer-invoice` (admin), `send-customer-invoice-to-contact-person`, `download-customer-invoice-pdf` (non-admins cannot download while approval is pending), `sync-customer-invoice` |
709
+ | **Customer invoices** | `check-customer-invoice-readiness`, `list-eligible-customer-invoice-budgets`, `preview-customer-invoice-amount`, `list-available-qbo-customer-invoices`, `list-customer-invoices` (global/project/budget filters; cursor pages via `--limit` / `--cursor` / `--direction` returning `{ items, pageInfo }`, `--page` / `--perPage` rejected; `--sortBy totalInvoiceAmount` sorts provider-confirmed totals including GST, with unknown amounts last), `count-customer-invoices` (`customerInvoice.getCounts`: filters only; `totalCount`, lifecycle status facets that ignore their own status filter, and amount metrics), `get-customer-invoice` (web-shaped batch), `get-customer-invoice-invoices`, `get-customer-invoice-lines`, `get-customer-invoice-approvals`, `get-customer-invoice-history`, `get-customer-invoice-email-context`, `preview-qbo-customer-invoice`, `import-qbo-customer-invoice` (BB-only link with reviewed token), `create-customer-invoice`, `resubmit-customer-invoice` (original requester only; one replacement after confirmed expiry voiding; repeated calls return the same replacement), `discard-customer-invoice`, `delete-customer-invoice`, `void-customer-invoice`, `approve-customer-invoice` (admin), `reject-customer-invoice` (admin), `send-customer-invoice-to-contact-person`, `download-customer-invoice-pdf` (non-admins cannot download while approval is pending), `sync-customer-invoice` |
702
710
  | **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` |
703
711
  | **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), `get-project-hub-status`, `setup-project-hub`, `sync-project-hub-commercial-documents`, `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`) |
704
712
  | **Contacts** | `list-contacts`, `create-contact-person` (`--payload`), `update-contact-person` (`--payload`) |
@@ -710,6 +718,12 @@ Admin/Lead users can run `bb supplier approve <supplierId>` to directly re-appro
710
718
 
711
719
  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.
712
720
 
721
+ ## Customer invoice approval expiry
722
+
723
+ New requests allow 72 elapsed hours and schedule one approver reminder four hours before the stored deadline. Older deadlines remain unchanged. Invoice lifecycle outputs/`--status` distinguish `EXPIRED` (approval elapsed, void not yet confirmed) from `EXPIRED_VOIDED`.
724
+
725
+ Use `bb customer-invoice resubmit <batchId>` (`resubmit-customer-invoice`) as the original requester after confirmed voiding. The normal financial/external-write/email confirmation gates apply. It queues one replacement from the current eligible estimate using the original percentage, due date and details; retries reuse the same replacement. The API rechecks readiness/coverage and does not send to the customer. Deploy the compatible API before this CLI. Legacy multi-invoice batches require manual creation.
726
+
713
727
  ## Output
714
728
 
715
729
  - **Canonical stdout:** a pretty-printed `{ "ok": true, "data": ..., "meta": ... }` JSON envelope
@@ -2317,7 +2317,7 @@
2317
2317
  {
2318
2318
  "path": ["bill", "create"],
2319
2319
  "legacyAliases": ["create-bill", "create_bill"],
2320
- "summary": "Create a bill or claim with an approved supplier. All amount fields exclude GST: amount is the portion billed; extractedAmount is the full invoice subtotal; lineAmounts and quotationAllocations also exclude GST. Example: 1800 + 162 GST = 1962 total requires amount=1800 and extractedAmount=1800 for a full bill. The CLI does not strip GST from input. --draft-only guarantees DRAFT without email or external writes; otherwise unpaid creation skips email permission but keeps external-write permission.",
2320
+ "summary": "Create a bill or claim with an approved supplier. All amount fields exclude GST: amount is the portion billed; extractedAmount is the full invoice subtotal; lineAmounts and quotationAllocations also exclude GST. Example: 1800 + 162 GST = 1962 total requires amount=1800 and extractedAmount=1800 for a full bill. The CLI does not strip GST from input. If the selected quotations cannot split the bill amount across every selected line (a line without a quoted amount that cannot be derived from the quotation total), pass lineAmounts; updates that keep the same amount and lines reuse the existing line amounts. --draft-only guarantees DRAFT without email or external writes; otherwise unpaid creation skips email permission but keeps external-write permission.",
2321
2321
  "globalOptions": [
2322
2322
  {
2323
2323
  "name": "--help",
@@ -2375,7 +2375,7 @@
2375
2375
  {
2376
2376
  "path": ["bill", "selection", "validate"],
2377
2377
  "legacyAliases": ["validate-bill-selection", "validate_bill_selection"],
2378
- "summary": "Validate the approved supplier and selected bill line items.",
2378
+ "summary": "Validate the approved supplier and selected bill line items. Supplier bills need approved quotation sources (quotationIds, legacy quotationId, or quotationAllocations with lineAmounts) for every line that is not quotation-exempt (Go Internal lines, or projects/budgets created before the quotation cutoff). With billId, lines already on the bill keep their stored sources unless new sources are supplied. Exemption is decided by the server, so there is no --quotation-exempt flag here. Amounts (amount, lineAmounts, quotationAllocations) are ex-GST dollars with at most 2 decimal places.",
2379
2379
  "globalOptions": [
2380
2380
  {
2381
2381
  "name": "--help",
@@ -2491,7 +2491,7 @@
2491
2491
  {
2492
2492
  "path": ["bill", "update"],
2493
2493
  "legacyAliases": ["update-bill", "update_bill"],
2494
- "summary": "Update a bill or claim with a partial payload including id; omitted fields are preserved. Comment-only updates are allowed on approved/paid bills without altering financial data or QuickBooks. All amount fields exclude GST: amount is the portion billed; extractedAmount is the full invoice subtotal; lineAmounts and quotationAllocations also exclude GST. Example: 1800 + 162 GST = 1962 total requires amount=1800 and extractedAmount=1800 for a full bill. The CLI does not strip GST from input. Re-read and verify both amount fields and allocations after correcting a gross/net mistake.",
2494
+ "summary": "Update a bill or claim with a partial payload including id; omitted fields are preserved. Comment-only updates are allowed on approved/paid bills without altering financial data or QuickBooks. All amount fields exclude GST: amount is the portion billed; extractedAmount is the full invoice subtotal; lineAmounts and quotationAllocations also exclude GST. Example: 1800 + 162 GST = 1962 total requires amount=1800 and extractedAmount=1800 for a full bill. The CLI does not strip GST from input. If the selected quotations cannot split the bill amount across every selected line (a line without a quoted amount that cannot be derived from the quotation total), pass lineAmounts; updates that keep the same amount and lines reuse the existing line amounts. Re-read and verify both amount fields and allocations after correcting a gross/net mistake.",
2495
2495
  "globalOptions": [
2496
2496
  {
2497
2497
  "name": "--help",
@@ -4937,6 +4937,56 @@
4937
4937
  "effects": ["state-change", "email", "external-write", "financial-write"],
4938
4938
  "legacyTarget": "create-customer-invoice"
4939
4939
  },
4940
+ {
4941
+ "path": ["customer-invoice", "resubmit"],
4942
+ "legacyAliases": ["resubmit-customer-invoice", "resubmit_customer_invoice"],
4943
+ "summary": "Resubmit a confirmed expired-and-voided BB invoice as its original requester. Argument: <batchId>. Queues one replacement using the current estimate, original percentage, due date and details; retries return the same replacement. No customer email.",
4944
+ "globalOptions": [
4945
+ {
4946
+ "name": "--help",
4947
+ "description": "Show help for this command."
4948
+ },
4949
+ {
4950
+ "name": "--quiet",
4951
+ "description": "Suppress non-error diagnostics."
4952
+ },
4953
+ {
4954
+ "name": "--debug",
4955
+ "description": "Emit sanitized diagnostic traces."
4956
+ },
4957
+ {
4958
+ "name": "--api-url",
4959
+ "description": "Override the Budget Builder API base URL."
4960
+ },
4961
+ {
4962
+ "name": "--no-browser",
4963
+ "description": "Print the authorization URL without opening a browser."
4964
+ },
4965
+ {
4966
+ "name": "--allow-state-change",
4967
+ "description": "Allow a Budget Builder state change in non-interactive use."
4968
+ },
4969
+ {
4970
+ "name": "--allow-email",
4971
+ "description": "Allow sending email in non-interactive use."
4972
+ },
4973
+ {
4974
+ "name": "--allow-external-write",
4975
+ "description": "Allow writes to external systems in non-interactive use."
4976
+ },
4977
+ {
4978
+ "name": "--allow-delete",
4979
+ "description": "Allow deleting data in non-interactive use."
4980
+ },
4981
+ {
4982
+ "name": "--allow-financial-write",
4983
+ "description": "Allow financial-record changes in non-interactive use."
4984
+ }
4985
+ ],
4986
+ "argumentMode": "legacy-passthrough",
4987
+ "effects": ["state-change", "external-write", "financial-write", "email"],
4988
+ "legacyTarget": "resubmit-customer-invoice"
4989
+ },
4940
4990
  {
4941
4991
  "path": ["customer-invoice", "discard"],
4942
4992
  "legacyAliases": ["discard-customer-invoice", "discard_customer_invoice"],
@@ -5193,7 +5243,7 @@
5193
5243
  "send-customer-invoice-to-contact-person",
5194
5244
  "send_customer_invoice_to_contact_person"
5195
5245
  ],
5196
- "summary": "Send a customer invoice and update the project billing email.",
5246
+ "summary": "Send a customer invoice and update the project billing email. Omit payload cc to use company billing defaults; an explicit list replaces them and [] removes them. Required internal Cc still applies.",
5197
5247
  "globalOptions": [
5198
5248
  {
5199
5249
  "name": "--help",
@@ -6081,7 +6131,7 @@
6081
6131
  {
6082
6132
  "path": ["company", "create"],
6083
6133
  "legacyAliases": ["create-company", "create_company"],
6084
- "summary": "Create or reuse a company in BB and QuickBooks. --name <name> --website <url>. Similar names require --confirmDistinct true; inactive companies are never reactivated.",
6134
+ "summary": "Create or reuse a company in BB and QuickBooks. --name <name> --website <url> [--billingEmails '<json-array>'] (up to 50 invoice Cc defaults, stored in BB only for new companies; reused company defaults are preserved). Similar names require --confirmDistinct true; inactive companies are never reactivated.",
6085
6135
  "globalOptions": [
6086
6136
  {
6087
6137
  "name": "--help",
@@ -6123,6 +6173,10 @@
6123
6173
  "name": "--allow-financial-write",
6124
6174
  "description": "Allow financial-record changes in non-interactive use."
6125
6175
  },
6176
+ {
6177
+ "name": "--billingEmails",
6178
+ "description": "JSON email array (max 50) for new company invoice Cc defaults; reused companies keep their saved defaults."
6179
+ },
6126
6180
  {
6127
6181
  "name": "--confirmDistinct",
6128
6182
  "description": "Set true to confirm a separate legal entity despite similar names."
@@ -6135,7 +6189,7 @@
6135
6189
  {
6136
6190
  "path": ["company", "update"],
6137
6191
  "legacyAliases": ["update-company", "update_company"],
6138
- "summary": "Save company fields in BB and queue independent QuickBooks name/website synchronization; returns operation ID, not provider completion.",
6192
+ "summary": "Save company fields in BB and queue independent QuickBooks name/website synchronization; returns operation ID, not provider completion. Optional payload billingEmails: email string array (max 50), omit to preserve defaults or [] to clear. Billing emails stay in BB; estimates are unchanged.",
6139
6193
  "globalOptions": [
6140
6194
  {
6141
6195
  "name": "--help",
@@ -10255,7 +10309,7 @@
10255
10309
  {
10256
10310
  "path": ["webhook", "delivery", "list"],
10257
10311
  "legacyAliases": ["list-webhook-deliveries", "list_webhook_deliveries"],
10258
- "summary": "List webhook deliveries.",
10312
+ "summary": "Admin-only: list webhook deliveries newest first. Returns { items, pageInfo }; follow pageInfo.nextCursor (or previousCursor with --direction backward). Cursors are opaque and filter-bound; raw delivery ids are rejected.",
10259
10313
  "globalOptions": [
10260
10314
  {
10261
10315
  "name": "--help",
@@ -10296,6 +10350,30 @@
10296
10350
  {
10297
10351
  "name": "--allow-financial-write",
10298
10352
  "description": "Allow financial-record changes in non-interactive use."
10353
+ },
10354
+ {
10355
+ "name": "--endpointId <id>",
10356
+ "description": "Only deliveries to this endpoint."
10357
+ },
10358
+ {
10359
+ "name": "--status <PENDING|SUCCEEDED|FAILED,...>",
10360
+ "description": "Comma-separated delivery statuses."
10361
+ },
10362
+ {
10363
+ "name": "--eventType <type>",
10364
+ "description": "Exact event type, e.g. bill.approved."
10365
+ },
10366
+ {
10367
+ "name": "--limit <n>",
10368
+ "description": "Rows per cursor page (1..100; default 20)."
10369
+ },
10370
+ {
10371
+ "name": "--cursor <token>",
10372
+ "description": "Opaque token from pageInfo.nextCursor or pageInfo.previousCursor; bound to the same filters."
10373
+ },
10374
+ {
10375
+ "name": "--direction <value>",
10376
+ "description": "forward or backward (default forward; backward requires cursor)."
10299
10377
  }
10300
10378
  ],
10301
10379
  "argumentMode": "legacy-passthrough",
@@ -396,7 +396,7 @@ Effects: state-change, email.
396
396
 
397
397
  ## `bb bill create`
398
398
 
399
- Create a bill or claim with an approved supplier. All amount fields exclude GST: amount is the portion billed; extractedAmount is the full invoice subtotal; lineAmounts and quotationAllocations also exclude GST. Example: 1800 + 162 GST = 1962 total requires amount=1800 and extractedAmount=1800 for a full bill. The CLI does not strip GST from input. --draft-only guarantees DRAFT without email or external writes; otherwise unpaid creation skips email permission but keeps external-write permission.
399
+ Create a bill or claim with an approved supplier. All amount fields exclude GST: amount is the portion billed; extractedAmount is the full invoice subtotal; lineAmounts and quotationAllocations also exclude GST. Example: 1800 + 162 GST = 1962 total requires amount=1800 and extractedAmount=1800 for a full bill. The CLI does not strip GST from input. If the selected quotations cannot split the bill amount across every selected line (a line without a quoted amount that cannot be derived from the quotation total), pass lineAmounts; updates that keep the same amount and lines reuse the existing line amounts. --draft-only guarantees DRAFT without email or external writes; otherwise unpaid creation skips email permission but keeps external-write permission.
400
400
 
401
401
  Legacy aliases: `create-bill`, `create_bill`.
402
402
 
@@ -409,7 +409,7 @@ Payload fields (--payload JSON)
409
409
 
410
410
  ## `bb bill selection validate`
411
411
 
412
- Validate the approved supplier and selected bill line items.
412
+ Validate the approved supplier and selected bill line items. Supplier bills need approved quotation sources (quotationIds, legacy quotationId, or quotationAllocations with lineAmounts) for every line that is not quotation-exempt (Go Internal lines, or projects/budgets created before the quotation cutoff). With billId, lines already on the bill keep their stored sources unless new sources are supplied. Exemption is decided by the server, so there is no --quotation-exempt flag here. Amounts (amount, lineAmounts, quotationAllocations) are ex-GST dollars with at most 2 decimal places.
413
413
 
414
414
  Legacy aliases: `validate-bill-selection`, `validate_bill_selection`.
415
415
 
@@ -435,7 +435,7 @@ Payload fields (--payload JSON)
435
435
 
436
436
  ## `bb bill update`
437
437
 
438
- Update a bill or claim with a partial payload including id; omitted fields are preserved. Comment-only updates are allowed on approved/paid bills without altering financial data or QuickBooks. All amount fields exclude GST: amount is the portion billed; extractedAmount is the full invoice subtotal; lineAmounts and quotationAllocations also exclude GST. Example: 1800 + 162 GST = 1962 total requires amount=1800 and extractedAmount=1800 for a full bill. The CLI does not strip GST from input. Re-read and verify both amount fields and allocations after correcting a gross/net mistake.
438
+ Update a bill or claim with a partial payload including id; omitted fields are preserved. Comment-only updates are allowed on approved/paid bills without altering financial data or QuickBooks. All amount fields exclude GST: amount is the portion billed; extractedAmount is the full invoice subtotal; lineAmounts and quotationAllocations also exclude GST. Example: 1800 + 162 GST = 1962 total requires amount=1800 and extractedAmount=1800 for a full bill. The CLI does not strip GST from input. If the selected quotations cannot split the bill amount across every selected line (a line without a quoted amount that cannot be derived from the quotation total), pass lineAmounts; updates that keep the same amount and lines reuse the existing line amounts. Re-read and verify both amount fields and allocations after correcting a gross/net mistake.
439
439
 
440
440
  Legacy aliases: `update-bill`, `update_bill`.
441
441
 
@@ -826,6 +826,14 @@ Payload fields (--payload JSON)
826
826
  Optional: requestId: UUID string (recommended); invoiceDate: ISO date; details: {billingAddress: string, customerMemo: string}; isCustomPercentage: boolean
827
827
  Example: bb create-customer-invoice --payload '{"budgetId":"<budget-id>","splits":[{"label":"Deposit","percentage":50,"dueDate":"2026-10-15"}],"details":{"billingAddress":"1 Example St","customerMemo":"Thanks"}}'
828
828
 
829
+ ## `bb customer-invoice resubmit`
830
+
831
+ Resubmit a confirmed expired-and-voided BB invoice as its original requester. Argument: <batchId>. Queues one replacement using the current estimate, original percentage, due date and details; retries return the same replacement. No customer email.
832
+
833
+ Legacy aliases: `resubmit-customer-invoice`, `resubmit_customer_invoice`.
834
+
835
+ Effects: state-change, external-write, financial-write, email.
836
+
829
837
  ## `bb customer-invoice discard`
830
838
 
831
839
  Discard Customer Invoice.
@@ -868,15 +876,15 @@ Effects: state-change, email, external-write, financial-write.
868
876
 
869
877
  ## `bb customer-invoice send`
870
878
 
871
- Send a customer invoice and update the project billing email.
879
+ Send a customer invoice and update the project billing email. Omit payload cc to use company billing defaults; an explicit list replaces them and [] removes them. Required internal Cc still applies.
872
880
 
873
881
  Legacy aliases: `send-customer-invoice-to-contact-person`, `send_customer_invoice_to_contact_person`.
874
882
 
875
883
  Effects: state-change, email.
876
884
 
877
885
  Payload fields (--payload JSON)
878
- Required: invoiceId: string; to: email; cc: email[]; replyTo: email; subject: string; content: HTML string; signature: HTML string
879
- Optional: none
886
+ Required: invoiceId: string; to: email; replyTo: email; subject: string; content: HTML string; signature: HTML string
887
+ Optional: cc: email[] (max 50; omit to use company billing defaults, explicit list replaces defaults, [] removes defaults; required internal Cc still applies)
880
888
  Example: bb send-customer-invoice-to-contact-person --payload '{"invoiceId":"<invoice-id>","to":"client@example.com","cc":[],"replyTo":"team@example.com","subject":"Invoice","content":"<p>Attached invoice</p>","signature":"<p>Team</p>"}'
881
889
 
882
890
  ## `bb customer-invoice pdf download`
@@ -1001,7 +1009,7 @@ Effects: state-change, external-write.
1001
1009
 
1002
1010
  ## `bb company create`
1003
1011
 
1004
- Create or reuse a company in BB and QuickBooks. --name <name> --website <url>. Similar names require --confirmDistinct true; inactive companies are never reactivated.
1012
+ Create or reuse a company in BB and QuickBooks. --name <name> --website <url> [--billingEmails '<json-array>'] (up to 50 invoice Cc defaults, stored in BB only for new companies; reused company defaults are preserved). Similar names require --confirmDistinct true; inactive companies are never reactivated.
1005
1013
 
1006
1014
  Legacy aliases: `create-company`, `create_company`.
1007
1015
 
@@ -1009,7 +1017,7 @@ Effects: state-change, external-write.
1009
1017
 
1010
1018
  ## `bb company update`
1011
1019
 
1012
- Save company fields in BB and queue independent QuickBooks name/website synchronization; returns operation ID, not provider completion.
1020
+ Save company fields in BB and queue independent QuickBooks name/website synchronization; returns operation ID, not provider completion. Optional payload billingEmails: email string array (max 50), omit to preserve defaults or [] to clear. Billing emails stay in BB; estimates are unchanged.
1013
1021
 
1014
1022
  Legacy aliases: `update-company`, `update_company`.
1015
1023
 
@@ -1017,8 +1025,8 @@ Effects: state-change, external-write.
1017
1025
 
1018
1026
  Payload fields (--payload JSON)
1019
1027
  Required: id: string; name: string; website: URL string
1020
- Optional: confirmDistinct: boolean
1021
- Example: bb update-company --payload '{"id":"<company-id>","name":"Example Co","website":"https://example.com"}'
1028
+ Optional: confirmDistinct: boolean; billingEmails: email string[] (max 50; omit to preserve, [] to clear; trimmed, lowercased and deduplicated by the API)
1029
+ Example: bb update-company --payload '{"id":"<company-id>","name":"Example Co","website":"https://example.com","billingEmails":["ap@example.com","finance@example.com"]}'
1022
1030
 
1023
1031
  ## `bb company sync status`
1024
1032
 
@@ -1657,7 +1665,7 @@ Effects: state-change.
1657
1665
 
1658
1666
  ## `bb webhook delivery list`
1659
1667
 
1660
- List webhook deliveries.
1668
+ Admin-only: list webhook deliveries newest first. Returns { items, pageInfo }; follow pageInfo.nextCursor (or previousCursor with --direction backward). Cursors are opaque and filter-bound; raw delivery ids are rejected.
1661
1669
 
1662
1670
  Legacy aliases: `list-webhook-deliveries`, `list_webhook_deliveries`.
1663
1671
 
package/dist/index.js CHANGED
@@ -7337,8 +7337,13 @@ var invoiceSplitSetupNextAction = (budgetId) => ({
7337
7337
  createCommand: "bb customer-invoice create --payload '<budgetId-requestId-invoiceDate-details-and-splits-json>'"
7338
7338
  });
7339
7339
 
7340
+ // ../utils/src/customer-invoice-approval-policy.ts
7341
+ var CUSTOMER_INVOICE_APPROVAL_WINDOW_MS = 72 * 60 * 60 * 1000;
7342
+ var CUSTOMER_INVOICE_APPROVAL_REMINDER_LEAD_MS = 4 * 60 * 60 * 1000;
7343
+
7340
7344
  // ../utils/src/customer-invoice-lifecycle.ts
7341
7345
  var customerInvoiceLifecycleStatuses = [
7346
+ "EXPIRED",
7342
7347
  ...Object.values(CustomerInvoiceBatchStatus),
7343
7348
  "PARTIALLY_SENT",
7344
7349
  CustomerInvoiceStatus.SENT,
@@ -7723,7 +7728,7 @@ var parseQuotationReferences = (input, commandName) => {
7723
7728
  const allocation = requireObject(raw, label);
7724
7729
  const amount = Number(allocation.amount);
7725
7730
  if (!Number.isFinite(amount) || amount < 0 || !Number.isInteger(amount * 100)) {
7726
- throw validationError(`${label}.amount must be a non-negative cent value.`);
7731
+ throw validationError(`${label}.amount must be a non-negative dollar amount (ex-GST) with at most 2 decimal places.`);
7727
7732
  }
7728
7733
  return {
7729
7734
  budgetItemId: requiredString(allocation.budgetItemId, `${label}.budgetItemId`),
@@ -7776,7 +7781,7 @@ var parseBillLineAmounts = (input, commandName) => {
7776
7781
  const line = requireObject(raw, label);
7777
7782
  const amount = Number(line.amount);
7778
7783
  if (!Number.isFinite(amount) || amount < 0 || !Number.isInteger(amount * 100)) {
7779
- throw validationError(`${label}.amount must be a non-negative cent value.`);
7784
+ throw validationError(`${label}.amount must be a non-negative dollar amount (ex-GST) with at most 2 decimal places.`);
7780
7785
  }
7781
7786
  return {
7782
7787
  budgetItemId: requiredString(line.budgetItemId, `${label}.budgetItemId`),
@@ -7870,8 +7875,10 @@ var parseSendEstimateToContactPersonPayload = (raw) => {
7870
7875
  };
7871
7876
  var parseSendCustomerInvoiceToContactPersonPayload = (raw) => {
7872
7877
  const input = requireObject(raw, "send-customer-invoice-to-contact-person payload");
7878
+ const { cc, ...email } = parseContactPersonEmailPayload({ ...input, cc: input.cc === undefined ? [] : input.cc }, "send-customer-invoice-to-contact-person");
7873
7879
  return {
7874
- ...parseContactPersonEmailPayload(input, "send-customer-invoice-to-contact-person"),
7880
+ ...email,
7881
+ ...input.cc === undefined ? {} : { cc },
7875
7882
  invoiceId: requiredString(input.invoiceId, "send-customer-invoice-to-contact-person payload.invoiceId")
7876
7883
  };
7877
7884
  };
@@ -8369,9 +8376,32 @@ function parseContactUpdatePayload(raw) {
8369
8376
  return raw;
8370
8377
  }
8371
8378
  function parseCompanyUpdatePayload(raw) {
8372
- requireObject(raw, "update-company payload");
8373
- return raw;
8379
+ const input = requireObject(raw, "update-company payload");
8380
+ const billingEmails = parseCompanyBillingEmails(input.billingEmails);
8381
+ if (input.confirmDistinct !== undefined && typeof input.confirmDistinct !== "boolean") {
8382
+ throw validationError("confirmDistinct must be a boolean.");
8383
+ }
8384
+ return {
8385
+ id: requiredString(input.id, "id"),
8386
+ name: requiredString(input.name, "name"),
8387
+ website: requiredString(input.website, "website"),
8388
+ ...input.confirmDistinct === undefined ? {} : { confirmDistinct: input.confirmDistinct },
8389
+ ...billingEmails === undefined ? {} : { billingEmails }
8390
+ };
8374
8391
  }
8392
+ var parseCompanyBillingEmails = (raw) => {
8393
+ if (raw === undefined)
8394
+ return;
8395
+ if (!Array.isArray(raw) || raw.length > 50) {
8396
+ throw validationError("billingEmails must be a JSON array of up to 50 email strings.");
8397
+ }
8398
+ return raw.map((email) => {
8399
+ if (typeof email !== "string") {
8400
+ throw validationError("billingEmails must contain only email strings.");
8401
+ }
8402
+ return email;
8403
+ });
8404
+ };
8375
8405
 
8376
8406
  // src/runtime/confirmation.ts
8377
8407
  var effectLabels = {
@@ -10031,6 +10061,14 @@ var createCustomerInvoice = async (raw) => {
10031
10061
  const result = await api2.customerInvoice.createInvoiceBatch.mutate(input);
10032
10062
  out(result);
10033
10063
  };
10064
+ var resubmitCustomerInvoice = async (batchId) => {
10065
+ await assertSensitiveWorkflowConfirmed({
10066
+ action: "Resubmit expired customer invoice",
10067
+ entity: `invoice batch ${batchId}`,
10068
+ details: "queues one replacement QuickBooks invoice after confirmed expiry voiding, using the current estimate and the original percentage, due date and details; only the original requester may resubmit; no customer email is sent"
10069
+ });
10070
+ out(await api2.customerInvoice.resubmitInvoiceBatch.mutate({ batchId }));
10071
+ };
10034
10072
  var discardCreatingCustomerInvoice = async (batchId) => {
10035
10073
  await assertSensitiveWorkflowConfirmed({
10036
10074
  action: "Discard unfinished customer invoice",
@@ -10108,7 +10146,7 @@ var syncCustomerInvoice = async (invoiceId) => {
10108
10146
  });
10109
10147
  out(result);
10110
10148
  };
10111
- var describeCustomerInvoiceEmailConfirmation = (input) => `emails the QBO invoice PDF to ${input.to} and saves that address as the project's billing email; requested CC ${input.cc.length > 0 ? input.cc.join(", ") : "none"}; reply-to ${input.replyTo}; subject "${input.subject}"; server-required admin, creator, business-development, and inside-sales CC recipients are added; success marks the invoice SENT; do not retry blindly after an ambiguous delivery failure`;
10149
+ var describeCustomerInvoiceEmailConfirmation = (input) => `emails the QBO invoice PDF to ${input.to} and saves that address as the project's billing email; requested CC ${input.cc === undefined ? "company billing defaults" : input.cc.length > 0 ? input.cc.join(", ") : "none (company billing defaults removed)"}; reply-to ${input.replyTo}; subject "${input.subject}"; server-required admin, creator, business-development, and inside-sales CC recipients are added; success marks the invoice SENT; do not retry blindly after an ambiguous delivery failure`;
10112
10150
  var sendCustomerInvoiceToContactPersonFromPayload = async (raw) => {
10113
10151
  const input = parseSendCustomerInvoiceToContactPersonPayload(raw);
10114
10152
  await assertSensitiveWorkflowConfirmed({
@@ -11071,8 +11109,8 @@ var payloadHelp = {
11071
11109
  },
11072
11110
  "update-company": {
11073
11111
  required: "id: string; name: string; website: URL string",
11074
- optional: "confirmDistinct: boolean",
11075
- example: '{"id":"<company-id>","name":"Example Co","website":"https://example.com"}'
11112
+ optional: "confirmDistinct: boolean; billingEmails: email string[] (max 50; omit to preserve, [] to clear; trimmed, lowercased and deduplicated by the API)",
11113
+ example: '{"id":"<company-id>","name":"Example Co","website":"https://example.com","billingEmails":["ap@example.com","finance@example.com"]}'
11076
11114
  },
11077
11115
  "update-project": {
11078
11116
  required: "id: string; name: string; companyId: string; contactPersonId: string; asanaTaskId: string; insideSalesId: string; businessDevelopmentId: string; venue: string; dateRange: {from: ISO date}",
@@ -11145,8 +11183,8 @@ var payloadHelp = {
11145
11183
  example: '{"budgetId":"<budget-id>","splits":[{"label":"Deposit","percentage":50,"dueDate":"2026-10-15"}],"details":{"billingAddress":"1 Example St","customerMemo":"Thanks"}}'
11146
11184
  },
11147
11185
  "send-customer-invoice-to-contact-person": {
11148
- required: "invoiceId: string; to: email; cc: email[]; replyTo: email; subject: string; content: HTML string; signature: HTML string",
11149
- optional: "none",
11186
+ required: "invoiceId: string; to: email; replyTo: email; subject: string; content: HTML string; signature: HTML string",
11187
+ optional: "cc: email[] (max 50; omit to use company billing defaults, explicit list replaces defaults, [] removes defaults; required internal Cc still applies)",
11150
11188
  example: '{"invoiceId":"<invoice-id>","to":"client@example.com","cc":[],"replyTo":"team@example.com","subject":"Invoice","content":"<p>Attached invoice</p>","signature":"<p>Team</p>"}'
11151
11189
  },
11152
11190
  "save-inbound-mapping": {
@@ -11354,6 +11392,12 @@ var deleteTargets = new Set([
11354
11392
  "update-project-status"
11355
11393
  ]);
11356
11394
  var explicitEffects = {
11395
+ "resubmit-customer-invoice": [
11396
+ "state-change",
11397
+ "external-write",
11398
+ "financial-write",
11399
+ "email"
11400
+ ],
11357
11401
  "rotate-webhook-secret": ["state-change"],
11358
11402
  "replay-webhook-delivery": ["state-change", "external-write"]
11359
11403
  };
@@ -11445,7 +11489,7 @@ var summaryFor = (legacyTarget) => {
11445
11489
  if (legacyTarget === "use-existing-company")
11446
11490
  return "Reuse a company in BB and QuickBooks, creating only its missing counterpart. Supply --companyId <bbId> and/or --quickbooksId <qboId>; both explicitly connect records with different names. --website <url> fills a missing QBO import website only. --confirmDistinct true confirms no similar record represents this entity.";
11447
11491
  if (legacyTarget === "create-company")
11448
- return "Create or reuse a company in BB and QuickBooks. --name <name> --website <url>. Similar names require --confirmDistinct true; inactive companies are never reactivated.";
11492
+ return "Create or reuse a company in BB and QuickBooks. --name <name> --website <url> [--billingEmails '<json-array>'] (up to 50 invoice Cc defaults, stored in BB only for new companies; reused company defaults are preserved). Similar names require --confirmDistinct true; inactive companies are never reactivated.";
11449
11493
  if (legacyTarget === "create-project")
11450
11494
  return "Create a project linked to an existing Asana Deals lead task. Supply --asanaTaskId in non-interactive use; interactive use searches for and selects a lead task (optionally --asanaSearch). The Slack channel is resolved from the selected Asana task unless all three Slack channel flags are supplied. For a new direct client, create the lead task using the website form first.";
11451
11495
  if (legacyTarget === "create-supplier")
@@ -11457,6 +11501,8 @@ var summaryFor = (legacyTarget) => {
11457
11501
  return "Preview amount before GST, GST, and total for a percentage of an accepted estimate. Arguments: <budgetId> <percentage-of-estimate>.";
11458
11502
  if (legacyTarget === "list-available-qbo-customer-invoices")
11459
11503
  return "List unlinked QuickBooks invoices available for an accepted or closed estimate. Argument: <budgetId>.";
11504
+ if (legacyTarget === "resubmit-customer-invoice")
11505
+ return "Resubmit a confirmed expired-and-voided BB invoice as its original requester. Argument: <batchId>. Queues one replacement using the current estimate, original percentage, due date and details; retries return the same replacement. No customer email.";
11460
11506
  if (legacyTarget === "create-customer-invoice")
11461
11507
  return "Create one invoice from an eligible budget using --payload JSON with invoice date, due date, billing address, customer memo, and a percentage of the original estimate that does not exceed its remaining coverage.";
11462
11508
  if (legacyTarget === "import-qbo-customer-invoice") {
@@ -11473,6 +11519,8 @@ var summaryFor = (legacyTarget) => {
11473
11519
  return "List one cursor page of active item categories ({ items, pageInfo }; --limit/--cursor/--direction, optional --name/--sortBy/--sortDir). --page/--perPage are rejected.";
11474
11520
  if (legacyTarget === "count-item-categories")
11475
11521
  return "Count active item categories matching --name: { totalCount }.";
11522
+ if (legacyTarget === "list-webhook-deliveries")
11523
+ return "Admin-only: list webhook deliveries newest first. Returns { items, pageInfo }; follow pageInfo.nextCursor (or previousCursor with --direction backward). Cursors are opaque and filter-bound; raw delivery ids are rejected.";
11476
11524
  if (legacyTarget === "list-integration-operations")
11477
11525
  return "List one cursor page of integration operations, newest first ({ items, pageInfo }; --limit/--cursor/--direction). --page/--perPage are rejected. Admin only.";
11478
11526
  if (legacyTarget === "count-integration-operations")
@@ -11491,17 +11539,17 @@ var summaryFor = (legacyTarget) => {
11491
11539
  return "Get budget details. Omit --includeInactive for active budgets only; --includeInactive is an Admin-only historical read.";
11492
11540
  }
11493
11541
  if (legacyTarget === "create-bill") {
11494
- return "Create a bill or claim with an approved supplier. All amount fields exclude GST: amount is the portion billed; extractedAmount is the full invoice subtotal; lineAmounts and quotationAllocations also exclude GST. Example: 1800 + 162 GST = 1962 total requires amount=1800 and extractedAmount=1800 for a full bill. The CLI does not strip GST from input. --draft-only guarantees DRAFT without email or external writes; otherwise unpaid creation skips email permission but keeps external-write permission.";
11542
+ return "Create a bill or claim with an approved supplier. All amount fields exclude GST: amount is the portion billed; extractedAmount is the full invoice subtotal; lineAmounts and quotationAllocations also exclude GST. Example: 1800 + 162 GST = 1962 total requires amount=1800 and extractedAmount=1800 for a full bill. The CLI does not strip GST from input. If the selected quotations cannot split the bill amount across every selected line (a line without a quoted amount that cannot be derived from the quotation total), pass lineAmounts; updates that keep the same amount and lines reuse the existing line amounts. --draft-only guarantees DRAFT without email or external writes; otherwise unpaid creation skips email permission but keeps external-write permission.";
11495
11543
  }
11496
11544
  if (legacyTarget === "record-external-bill-payment")
11497
11545
  return "Record an already-made payment on an APPROVED bill or claim, with no email and no money transfer. Requires <billId> --paymentReference --paymentDate YYYY-MM-DD --paymentAccount <QBO account ID> [--paymentProof receipt.pdf]. Reuses settled QBO payment links or queues one dated payment from the selected account. Exact retries are safe; do not pay again while reconciliation is pending.";
11498
11546
  if (legacyTarget === "list-external-payment-accounts")
11499
11547
  return "List active QuickBooks bank and credit-card account IDs for recording external payments (admin only).";
11500
11548
  if (legacyTarget === "update-bill") {
11501
- return "Update a bill or claim with a partial payload including id; omitted fields are preserved. Comment-only updates are allowed on approved/paid bills without altering financial data or QuickBooks. All amount fields exclude GST: amount is the portion billed; extractedAmount is the full invoice subtotal; lineAmounts and quotationAllocations also exclude GST. Example: 1800 + 162 GST = 1962 total requires amount=1800 and extractedAmount=1800 for a full bill. The CLI does not strip GST from input. Re-read and verify both amount fields and allocations after correcting a gross/net mistake.";
11549
+ return "Update a bill or claim with a partial payload including id; omitted fields are preserved. Comment-only updates are allowed on approved/paid bills without altering financial data or QuickBooks. All amount fields exclude GST: amount is the portion billed; extractedAmount is the full invoice subtotal; lineAmounts and quotationAllocations also exclude GST. Example: 1800 + 162 GST = 1962 total requires amount=1800 and extractedAmount=1800 for a full bill. The CLI does not strip GST from input. If the selected quotations cannot split the bill amount across every selected line (a line without a quoted amount that cannot be derived from the quotation total), pass lineAmounts; updates that keep the same amount and lines reuse the existing line amounts. Re-read and verify both amount fields and allocations after correcting a gross/net mistake.";
11502
11550
  }
11503
11551
  if (legacyTarget === "validate-bill-selection") {
11504
- return "Validate the approved supplier and selected bill line items.";
11552
+ return "Validate the approved supplier and selected bill line items. Supplier bills need approved quotation sources (quotationIds, legacy quotationId, or quotationAllocations with lineAmounts) for every line that is not quotation-exempt (Go Internal lines, or projects/budgets created before the quotation cutoff). With billId, lines already on the bill keep their stored sources unless new sources are supplied. Exemption is decided by the server, so there is no --quotation-exempt flag here. Amounts (amount, lineAmounts, quotationAllocations) are ex-GST dollars with at most 2 decimal places.";
11505
11553
  }
11506
11554
  if (legacyTarget === "create-quotation") {
11507
11555
  return 'Create a quotation with an approved supplier using --payload JSON. Include projectId, budgetId, supplierId, budgetItemIds, amountWithoutGst, gstAmount, and one uploaded attachment in attachments. Multiple budget items require lineAmounts: [{"budgetItemId":"<first-id>","amount":750},{"budgetItemId":"<second-id>","amount":450}], covering every selected budget-item ID exactly once and summing to amountWithoutGst. Amounts exclude GST; use gstAmount: 0 for no GST. Example: amountWithoutGst: 1200 with line amounts 750 and 450.';
@@ -11515,10 +11563,10 @@ var summaryFor = (legacyTarget) => {
11515
11563
  return "Download a customer invoice PDF when permitted.";
11516
11564
  }
11517
11565
  if (legacyTarget === "send-customer-invoice-to-contact-person") {
11518
- return "Send a customer invoice and update the project billing email.";
11566
+ return "Send a customer invoice and update the project billing email. Omit payload cc to use company billing defaults; an explicit list replaces them and [] removes them. Required internal Cc still applies.";
11519
11567
  }
11520
11568
  if (legacyTarget === "update-company")
11521
- return "Save company fields in BB and queue independent QuickBooks name/website synchronization; returns operation ID, not provider completion.";
11569
+ return "Save company fields in BB and queue independent QuickBooks name/website synchronization; returns operation ID, not provider completion. Optional payload billingEmails: email string array (max 50), omit to preserve defaults or [] to clear. Billing emails stay in BB; estimates are unchanged.";
11522
11570
  if (legacyTarget === "get-company-quickbooks-sync-status")
11523
11571
  return "Read saved company synchronization status without provider calls or polling; use companyId.";
11524
11572
  if (legacyTarget === "get-item-quickbooks-sync-status")
@@ -11981,6 +12029,7 @@ var registry2 = [
11981
12029
  "import-qbo"
11982
12030
  ]),
11983
12031
  legacyCommand("create-customer-invoice", ["customer-invoice", "create"]),
12032
+ legacyCommand("resubmit-customer-invoice", ["customer-invoice", "resubmit"]),
11984
12033
  legacyCommand("discard-customer-invoice", ["customer-invoice", "discard"]),
11985
12034
  legacyCommand("delete-customer-invoice", ["customer-invoice", "delete"]),
11986
12035
  legacyCommand("void-customer-invoice", ["customer-invoice", "void"]),
@@ -12094,6 +12143,10 @@ var registry2 = [
12094
12143
  }
12095
12144
  ]),
12096
12145
  legacyCommand("create-company", ["company", "create"], [], [
12146
+ {
12147
+ name: "--billingEmails",
12148
+ description: "JSON email array (max 50) for new company invoice Cc defaults; reused companies keep their saved defaults."
12149
+ },
12097
12150
  {
12098
12151
  name: "--confirmDistinct",
12099
12152
  description: "Set true to confirm a separate legal entity despite similar names."
@@ -12490,7 +12543,32 @@ var registry2 = [
12490
12543
  legacyCommand("delete-webhook-endpoint", ["webhook", "endpoint", "delete"]),
12491
12544
  legacyCommand("rotate-webhook-secret", ["webhook", "secret", "rotate"]),
12492
12545
  legacyCommand("send-webhook-test-event", ["webhook", "endpoint", "test"]),
12493
- legacyCommand("list-webhook-deliveries", ["webhook", "delivery", "list"]),
12546
+ legacyCommand("list-webhook-deliveries", ["webhook", "delivery", "list"], [], [
12547
+ {
12548
+ name: "--endpointId <id>",
12549
+ description: "Only deliveries to this endpoint."
12550
+ },
12551
+ {
12552
+ name: "--status <PENDING|SUCCEEDED|FAILED,...>",
12553
+ description: "Comma-separated delivery statuses."
12554
+ },
12555
+ {
12556
+ name: "--eventType <type>",
12557
+ description: "Exact event type, e.g. bill.approved."
12558
+ },
12559
+ {
12560
+ name: "--limit <n>",
12561
+ description: "Rows per cursor page (1..100; default 20)."
12562
+ },
12563
+ {
12564
+ name: "--cursor <token>",
12565
+ description: "Opaque token from pageInfo.nextCursor or pageInfo.previousCursor; bound to the same filters."
12566
+ },
12567
+ {
12568
+ name: "--direction <value>",
12569
+ description: "forward or backward (default forward; backward requires cursor)."
12570
+ }
12571
+ ]),
12494
12572
  legacyCommand("get-webhook-delivery", ["webhook", "delivery", "get"]),
12495
12573
  legacyCommand("replay-webhook-delivery", ["webhook", "delivery", "replay"]),
12496
12574
  legacyCommand("list-inbound-sources", ["inbound", "source", "list"], [], [
@@ -13415,6 +13493,7 @@ Customer invoices
13415
13493
  discard-customer-invoice <batchId> Discard a reserved CREATING batch with no created QBO invoices.
13416
13494
  delete-customer-invoice <batchId> Delete QBO invoices and the local batch.
13417
13495
  void-customer-invoice <batchId> Void the invoice batch in QuickBooks.
13496
+ resubmit-customer-invoice <batchId> Original requester only. Queues one replacement after confirmed expiry voiding; retries return the same replacement. Never sends to the customer.
13418
13497
  approve-customer-invoice <batchId> Admin approval; rejects voided invoices, notifies the creator, and closes the estimate at cumulative 100% coverage.
13419
13498
  reject-customer-invoice <batchId> --reason <text> Admin rejection; voids QBO invoices and notifies the creator.
13420
13499
  send-customer-invoice-to-contact-person --payload '<json>' Queues same composer workflow as web; requires invoiceId, to, cc, replyTo, subject, HTML content, and HTML signature.
@@ -13440,9 +13519,9 @@ Companies & projects
13440
13519
  use-existing-company --companyId <bbId> and/or --quickbooksId <qboId> [--website <url>] [--confirmDistinct true]
13441
13520
  Supply both IDs to explicitly connect existing records with different names.
13442
13521
  Reuses both records, creating only the missing counterpart. Website is needed only for a QBO import without one.
13443
- create-company --name --website <url> [--confirmDistinct true]
13522
+ create-company --name --website <url> [--confirmDistinct true] [--billingEmails '<json-array>']
13444
13523
  Exact existing companies are reused. Similar names require explicit separate-entity confirmation.
13445
- update-company --payload '<json>' (saves BB fields; returns queued QBO operation ID, not completion)
13524
+ update-company --payload '<json>' (optional billingEmails: omit to preserve, [] to clear; returns queued QBO operation ID, not completion)
13446
13525
  get-company-quickbooks-sync-status <companyId> (saved status only; no provider call or polling)
13447
13526
  delete-company <id>
13448
13527
  list-projects [--companyId] [--name] [--status|--statuses CSV] [--active true|false] [--wonFrom ISO] [--wonTo ISO] [--wonWithinDays N] [--limit 1-100] [--cursor] [--direction forward|backward] [--sortBy name|createdAt|status|companyName|wonAt] [--sortDir asc|desc]
@@ -13544,7 +13623,7 @@ Webhooks (admin)
13544
13623
  delete-webhook-endpoint <endpointId>
13545
13624
  rotate-webhook-secret <endpointId> [--previousSecretTtlHours <0-168>] (prints the new secret once)
13546
13625
  send-webhook-test-event <endpointId>
13547
- list-webhook-deliveries [--endpointId] [--status PENDING|SUCCEEDED|FAILED] [--eventType] [--cursor] [--limit]
13626
+ list-webhook-deliveries [--endpointId] [--status <PENDING|SUCCEEDED|FAILED,...>] [--eventType] [--limit 1-100] [--cursor <token>] [--direction forward|backward] (newest first; prints items + pageInfo)
13548
13627
  get-webhook-delivery <deliveryId> (attempts with response codes)
13549
13628
  replay-webhook-delivery <deliveryId> (same eventId, new attempt chain)
13550
13629
 
@@ -14911,6 +14990,13 @@ Waiting for approval\u2026
14911
14990
  await createCustomerInvoice(parseJsonFlag(String(payloadRaw), "--payload"));
14912
14991
  break;
14913
14992
  }
14993
+ case "resubmit-customer-invoice": {
14994
+ const batchId = positional[0];
14995
+ if (!batchId)
14996
+ throw new Error("resubmit-customer-invoice requires <batchId>");
14997
+ await resubmitCustomerInvoice(batchId);
14998
+ break;
14999
+ }
14914
15000
  case "discard-customer-invoice": {
14915
15001
  const batchId = positional[0];
14916
15002
  if (!batchId) {
@@ -15055,6 +15141,9 @@ Waiting for approval\u2026
15055
15141
  break;
15056
15142
  }
15057
15143
  case "create-company": {
15144
+ rejectBareFlag(flags, "billingEmails");
15145
+ const billingEmailsRaw = getFlag(flags, "billingEmails");
15146
+ const billingEmails = billingEmailsRaw === undefined ? undefined : parseCompanyBillingEmails(parseJsonFlag(billingEmailsRaw, "--billingEmails"));
15058
15147
  const name = getFlag(flags, "name");
15059
15148
  const website = getFlag(flags, "website");
15060
15149
  if (!name || !website) {
@@ -15063,6 +15152,7 @@ Waiting for approval\u2026
15063
15152
  await createCompany({
15064
15153
  name,
15065
15154
  website,
15155
+ ...billingEmails === undefined ? {} : { billingEmails },
15066
15156
  confirmDistinct: parseOptionalBoolFlag(flags, "confirmDistinct")
15067
15157
  });
15068
15158
  break;
@@ -15922,14 +16012,22 @@ Waiting for approval\u2026
15922
16012
  break;
15923
16013
  }
15924
16014
  case "list-webhook-deliveries": {
15925
- const status = getFlag(flags, "status");
15926
- if (status && status !== "PENDING" && status !== "SUCCEEDED" && status !== "FAILED")
15927
- throw new Error("--status must be PENDING, SUCCEEDED or FAILED");
16015
+ const statuses = (getFlag(flags, "status") ?? "").split(",").map((value) => value.trim()).filter(Boolean);
16016
+ for (const status of statuses)
16017
+ if (status !== "PENDING" && status !== "SUCCEEDED" && status !== "FAILED")
16018
+ throw new Error("--status must be PENDING, SUCCEEDED or FAILED (comma-separated)");
16019
+ const cursor = getFlag(flags, "cursor");
16020
+ const direction = parseProjectDirection(getFlag(flags, "direction"));
16021
+ if (direction === "backward" && !cursor)
16022
+ throw new Error("--direction backward requires --cursor <token>.");
15928
16023
  await listWebhookDeliveries({
15929
16024
  endpointId: getFlag(flags, "endpointId"),
15930
- status,
16025
+ ...statuses.length ? {
16026
+ status: statuses
16027
+ } : {},
15931
16028
  eventType: getFlag(flags, "eventType"),
15932
- cursor: getFlag(flags, "cursor"),
16029
+ cursor,
16030
+ direction,
15933
16031
  limit: parsePositiveIntFlag(getFlag(flags, "limit"), "--limit")
15934
16032
  });
15935
16033
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@go-labs-sg/bb",
3
- "version": "3.1.0",
3
+ "version": "3.3.0",
4
4
  "description": "Budget Builder CLI for AI agents — manage budgets, bills, claims, quotations, and customer invoices with explicit workflow previews for sensitive changes.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",