@go-labs-sg/bb 3.2.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
@@ -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",
@@ -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
 
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,
@@ -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") {
@@ -11517,10 +11563,10 @@ var summaryFor = (legacyTarget) => {
11517
11563
  return "Download a customer invoice PDF when permitted.";
11518
11564
  }
11519
11565
  if (legacyTarget === "send-customer-invoice-to-contact-person") {
11520
- 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.";
11521
11567
  }
11522
11568
  if (legacyTarget === "update-company")
11523
- 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.";
11524
11570
  if (legacyTarget === "get-company-quickbooks-sync-status")
11525
11571
  return "Read saved company synchronization status without provider calls or polling; use companyId.";
11526
11572
  if (legacyTarget === "get-item-quickbooks-sync-status")
@@ -11983,6 +12029,7 @@ var registry2 = [
11983
12029
  "import-qbo"
11984
12030
  ]),
11985
12031
  legacyCommand("create-customer-invoice", ["customer-invoice", "create"]),
12032
+ legacyCommand("resubmit-customer-invoice", ["customer-invoice", "resubmit"]),
11986
12033
  legacyCommand("discard-customer-invoice", ["customer-invoice", "discard"]),
11987
12034
  legacyCommand("delete-customer-invoice", ["customer-invoice", "delete"]),
11988
12035
  legacyCommand("void-customer-invoice", ["customer-invoice", "void"]),
@@ -12096,6 +12143,10 @@ var registry2 = [
12096
12143
  }
12097
12144
  ]),
12098
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
+ },
12099
12150
  {
12100
12151
  name: "--confirmDistinct",
12101
12152
  description: "Set true to confirm a separate legal entity despite similar names."
@@ -13442,6 +13493,7 @@ Customer invoices
13442
13493
  discard-customer-invoice <batchId> Discard a reserved CREATING batch with no created QBO invoices.
13443
13494
  delete-customer-invoice <batchId> Delete QBO invoices and the local batch.
13444
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.
13445
13497
  approve-customer-invoice <batchId> Admin approval; rejects voided invoices, notifies the creator, and closes the estimate at cumulative 100% coverage.
13446
13498
  reject-customer-invoice <batchId> --reason <text> Admin rejection; voids QBO invoices and notifies the creator.
13447
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.
@@ -13467,9 +13519,9 @@ Companies & projects
13467
13519
  use-existing-company --companyId <bbId> and/or --quickbooksId <qboId> [--website <url>] [--confirmDistinct true]
13468
13520
  Supply both IDs to explicitly connect existing records with different names.
13469
13521
  Reuses both records, creating only the missing counterpart. Website is needed only for a QBO import without one.
13470
- create-company --name --website <url> [--confirmDistinct true]
13522
+ create-company --name --website <url> [--confirmDistinct true] [--billingEmails '<json-array>']
13471
13523
  Exact existing companies are reused. Similar names require explicit separate-entity confirmation.
13472
- 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)
13473
13525
  get-company-quickbooks-sync-status <companyId> (saved status only; no provider call or polling)
13474
13526
  delete-company <id>
13475
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]
@@ -14938,6 +14990,13 @@ Waiting for approval\u2026
14938
14990
  await createCustomerInvoice(parseJsonFlag(String(payloadRaw), "--payload"));
14939
14991
  break;
14940
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
+ }
14941
15000
  case "discard-customer-invoice": {
14942
15001
  const batchId = positional[0];
14943
15002
  if (!batchId) {
@@ -15082,6 +15141,9 @@ Waiting for approval\u2026
15082
15141
  break;
15083
15142
  }
15084
15143
  case "create-company": {
15144
+ rejectBareFlag(flags, "billingEmails");
15145
+ const billingEmailsRaw = getFlag(flags, "billingEmails");
15146
+ const billingEmails = billingEmailsRaw === undefined ? undefined : parseCompanyBillingEmails(parseJsonFlag(billingEmailsRaw, "--billingEmails"));
15085
15147
  const name = getFlag(flags, "name");
15086
15148
  const website = getFlag(flags, "website");
15087
15149
  if (!name || !website) {
@@ -15090,6 +15152,7 @@ Waiting for approval\u2026
15090
15152
  await createCompany({
15091
15153
  name,
15092
15154
  website,
15155
+ ...billingEmails === undefined ? {} : { billingEmails },
15093
15156
  confirmDistinct: parseOptionalBoolFlag(flags, "confirmDistinct")
15094
15157
  });
15095
15158
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@go-labs-sg/bb",
3
- "version": "3.2.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",