@go-labs-sg/bb 2.33.1 → 2.34.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 +53 -1
- package/command-manifest.json +141 -3
- package/command-reference.md +19 -3
- package/dist/index.js +299 -157
- package/package.json +1 -1
- package/role-aware-agent-guide.md +40 -0
package/README.md
CHANGED
|
@@ -12,6 +12,16 @@ For AI agents using this CLI: do not execute mutations until the user explicitly
|
|
|
12
12
|
|
|
13
13
|
**Registry:** [`@go-labs-sg/bb`](https://www.npmjs.com/package/@go-labs-sg/bb)
|
|
14
14
|
|
|
15
|
+
## Company discovery and reuse
|
|
16
|
+
|
|
17
|
+
`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.
|
|
18
|
+
|
|
19
|
+
After reviewing a result, `bb company use --companyId <bbId>` reuses BB and resolves its QBO customer. `bb company use --quickbooksId <qboId>` imports the existing QBO name/website when BB is missing. Supply both IDs to explicitly connect two unlinked records whose names differ; existing company details are preserved. If QBO has no website, add `--website <url>` for the BB import only.
|
|
20
|
+
|
|
21
|
+
The shared API rechecks existing records at submission. Canonical matches reuse records; fuzzy matches require an explicit choice or `--confirmDistinct true` to confirm a separate legal entity. Inactive companies and conflicting links require review, never implicit reactivation. QBO project/sub-customers are not company-import candidates. Provider failures are not treated as missing records.
|
|
22
|
+
|
|
23
|
+
`bb company use` and `bb company create` are state-changing, potentially external-writing commands. In non-interactive mode their existing permission gates require `--allow-state-change --allow-external-write`. Deploy the backend before publishing this CLI change. See [company discovery details](../api/docs/company-discovery.md) for limits and retry behaviour.
|
|
24
|
+
|
|
15
25
|
## Shared catalogue editing
|
|
16
26
|
|
|
17
27
|
All active authenticated users can edit suppliers, inventory items and item categories. Normal-user supplier updates submit a revision for approval in the shared API; item/category edits apply immediately. Lead/Admin supplier edits bypass human review. `bb supplier update` returns the server-owned approval/synchronization result without issuing another approval request. New supplier usage stays on hold during review and linked QuickBooks synchronization; existing usage continues with the last approved details until the edit is approved. Creator identity and delete/archive permissions are unchanged.
|
|
@@ -286,6 +296,48 @@ Bill creation determines its required permissions from the parsed input, for bot
|
|
|
286
296
|
|
|
287
297
|
**Contact-person estimate email:** `bb budget estimate send --payload '<json>'` calls the same `email.sendEstimateToContactPerson` procedure as the web composer. The payload requires `budgetId`, `estimateId`, optional `estimateDocNumber`, `to`, `cc`, `replyTo`, `subject`, HTML `content`, and HTML `signature`. It durably queues worker delivery of the QuickBooks PDF, standard terms, and Budget Builder budget attachments, and returns the outbound-email and operation IDs immediately. Worker retries reuse one provider idempotency key; the budget becomes `ESTIMATE_SENT` only after Resend accepts the email. The command requires interactive `CONFIRM`; inspect the budget, recipients, and HTML first.
|
|
288
298
|
|
|
299
|
+
### Quotation amounts and line allocations
|
|
300
|
+
|
|
301
|
+
Quotation create and update payloads use `amountWithoutGst` for the supplier subtotal and `gstAmount` for GST. Keep GST separate; do not add it to `amountWithoutGst`. When a quotation covers more than one budget item, `lineAmounts` is required and must contain each selected item exactly once. The line amounts must sum to `amountWithoutGst`.
|
|
302
|
+
|
|
303
|
+
Use the actual `budgetItemIds` returned by `bb budget get <budget-id>` or the relevant budget-item read. Do not invent IDs, use item names, or reuse IDs from another project or budget. `quotationAllocations` belongs to bill creation/update as the bill's source-quotation allocation; it is not a quotation create/update field.
|
|
304
|
+
|
|
305
|
+
Upload exactly one quotation document first. Pass the single attachment object returned by `bb quotation attachment upload <project-id> <file-path>` in the quotation payload's one-element `attachments` array. The server accepts one original quotation file and normalizes it to PDF when needed.
|
|
306
|
+
|
|
307
|
+
Replace the example attachment size `12345` with the actual byte count from the upload response.
|
|
308
|
+
|
|
309
|
+
For a quotation split across two real budget lines at S$750 and S$450, with no GST, the complete CLI payload shape is:
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
bb quotation attachment upload "<project-id>" "<quotation-file.pdf>"
|
|
313
|
+
|
|
314
|
+
# Replace every <...> placeholder with the matching value from the reads and
|
|
315
|
+
# the upload response. The attachment array must contain that one upload object.
|
|
316
|
+
bb quotation create --payload '{
|
|
317
|
+
"projectId": "<project-id>",
|
|
318
|
+
"budgetId": "<budget-id>",
|
|
319
|
+
"supplierId": "<supplier-id>",
|
|
320
|
+
"budgetItemIds": ["<budget-item-id-for-750>", "<budget-item-id-for-450>"],
|
|
321
|
+
"lineAmounts": [
|
|
322
|
+
{"budgetItemId": "<budget-item-id-for-750>", "amount": 750},
|
|
323
|
+
{"budgetItemId": "<budget-item-id-for-450>", "amount": 450}
|
|
324
|
+
],
|
|
325
|
+
"amountWithoutGst": 1200,
|
|
326
|
+
"gstAmount": 0,
|
|
327
|
+
"attachments": [
|
|
328
|
+
{
|
|
329
|
+
"id": "<attachment-id-from-upload>",
|
|
330
|
+
"name": "<attachment-name-from-upload>",
|
|
331
|
+
"key": "<attachment-key-from-upload>",
|
|
332
|
+
"size": 12345
|
|
333
|
+
}
|
|
334
|
+
]
|
|
335
|
+
}'
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Use the same fields for `bb quotation update --payload`, adding the existing quotation `"id": "<quotation-id>"`. Read the quotation and its budget before updating so the IDs and current status are confirmed. The example's subtotal is S$1,200, GST is S$0, and total is S$1,200; those values are illustrative only and contain no live resource IDs.
|
|
339
|
+
|
|
340
|
+
|
|
289
341
|
### Link an existing QuickBooks invoice
|
|
290
342
|
|
|
291
343
|
**Release status:** these CLI commands are pending release. The repository package version remains `2.31.0`; that version number alone does not establish that a published CLI contains them. Check `bb help` for `customer-invoice preview-qbo` and `customer-invoice import-qbo` after the normal CLI release. This code delivery does not publish the CLI, deploy the API, or perform a production import. The backend must expose `customerInvoice.previewExistingInvoice` and `customerInvoice.linkExistingInvoice` (implemented in PR #1067).
|
|
@@ -358,7 +410,7 @@ Bill, claim, and quotation mutations accept only suppliers whose approval status
|
|
|
358
410
|
| --- | --- |
|
|
359
411
|
| **Budgets** | `list-budgets` (full payload by default; `--summary` or `--includeDetails false` for slim list), `get-budget`, `get-budget-items`, `get-budget-details`, `get-budget-categories`, `get-budget-versions`, `rename-budget-version`, `restore-budget-version`, `update-budget-status` (`ESTIMATE_ACCEPTED` requires win proof, auto-marks `PITCH` or `LOST` projects `WON`, and auto-creates an Asana Event Ops section from Prompt 5 tasks when enabled; Prompt 5 skips quantity-zero items, combines matching non-GO-internal suppliers, keeps each GO internal item separate, removes generated work that repeats another line item or group, and schedules applicable subtasks relative to the project start date; `--projectStatusOnCommercialRejection PITCH\|LOST` is required when rejecting the only accepted/closed budget on a commercial project), `mark-budget-won` (`<budgetId>` + proof file path; `PITCH` or `LOST` projects become `WON` automatically), `create-budget` / `update-budget` (`--payload`; 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`) |
|
|
360
412
|
| **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`, `upload-bill-attachment` (`<billId>` + local path), `get-bill-details` |
|
|
361
|
-
| **Quotations** | `list-quotations` (supports project, budget, supplier, status, requester, and text-search filters), `get-quotation`, `upload-quotation-attachment` (`<projectId>` + PDF/JPEG/PNG path up to 20MB; returns attachment JSON for payload use), `cleanup-staged-quotation-attachments`, `create-quotation` (`--payload` for `quotation.createDraft`; include `amountWithoutGst` and `
|
|
413
|
+
| **Quotations** | `list-quotations` (supports project, budget, supplier, status, requester, and text-search filters), `get-quotation`, `upload-quotation-attachment` (`<projectId>` + PDF/JPEG/PNG path up to 20MB; returns attachment JSON for payload use), `cleanup-staged-quotation-attachments`, `create-quotation` (`--payload` for `quotation.createDraft`; include `amountWithoutGst`, `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`) |
|
|
362
414
|
| **Customer invoices** | `check-customer-invoice-readiness`, `list-eligible-customer-invoice-budgets`, `list-customer-invoices` (global/project/budget filters plus summary metrics; `--sortBy totalInvoiceAmount` sorts provider-confirmed totals including GST, with unknown amounts last), `get-customer-invoice`, `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` |
|
|
363
415
|
| **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` |
|
|
364
416
|
| **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`) |
|
package/command-manifest.json
CHANGED
|
@@ -3107,7 +3107,7 @@
|
|
|
3107
3107
|
{
|
|
3108
3108
|
"path": ["quotation", "create"],
|
|
3109
3109
|
"legacyAliases": ["create-quotation", "create_quotation"],
|
|
3110
|
-
"summary": "Create a quotation with an approved supplier.",
|
|
3110
|
+
"summary": "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.",
|
|
3111
3111
|
"globalOptions": [
|
|
3112
3112
|
{
|
|
3113
3113
|
"name": "--help",
|
|
@@ -3148,6 +3148,11 @@
|
|
|
3148
3148
|
{
|
|
3149
3149
|
"name": "--allow-financial-write",
|
|
3150
3150
|
"description": "Allow financial-record changes in non-interactive use."
|
|
3151
|
+
},
|
|
3152
|
+
{
|
|
3153
|
+
"name": "--payload",
|
|
3154
|
+
"description": "Quotation JSON including lineAmounts for multiple selected budget items; see the payload format above.",
|
|
3155
|
+
"required": true
|
|
3151
3156
|
}
|
|
3152
3157
|
],
|
|
3153
3158
|
"argumentMode": "legacy-passthrough",
|
|
@@ -3157,7 +3162,7 @@
|
|
|
3157
3162
|
{
|
|
3158
3163
|
"path": ["quotation", "update"],
|
|
3159
3164
|
"legacyAliases": ["update-quotation", "update_quotation"],
|
|
3160
|
-
"summary": "Update a quotation with
|
|
3165
|
+
"summary": "Update a DRAFT or REJECTED quotation using --payload JSON with id and the complete creation payload. Multiple budget items require lineAmounts: [{\"budgetItemId\":\"<first-id>\",\"amount\":750},{\"budgetItemId\":\"<second-id>\",\"amount\":450}], covering every budgetItemIds entry exactly once and summing to amountWithoutGst. Amounts exclude GST; use gstAmount: 0 for no GST.",
|
|
3161
3166
|
"globalOptions": [
|
|
3162
3167
|
{
|
|
3163
3168
|
"name": "--help",
|
|
@@ -3198,6 +3203,11 @@
|
|
|
3198
3203
|
{
|
|
3199
3204
|
"name": "--allow-financial-write",
|
|
3200
3205
|
"description": "Allow financial-record changes in non-interactive use."
|
|
3206
|
+
},
|
|
3207
|
+
{
|
|
3208
|
+
"name": "--payload",
|
|
3209
|
+
"description": "Complete quotation JSON plus id, including lineAmounts for multiple selected budget items.",
|
|
3210
|
+
"required": true
|
|
3201
3211
|
}
|
|
3202
3212
|
],
|
|
3203
3213
|
"argumentMode": "legacy-passthrough",
|
|
@@ -4780,10 +4790,134 @@
|
|
|
4780
4790
|
"effects": [],
|
|
4781
4791
|
"legacyTarget": "get-company"
|
|
4782
4792
|
},
|
|
4793
|
+
{
|
|
4794
|
+
"path": ["company", "search"],
|
|
4795
|
+
"legacyAliases": ["search-existing-companies", "search_existing_companies"],
|
|
4796
|
+
"summary": "Search BB and QuickBooks companies without writes. --query <name> [--page <number>].",
|
|
4797
|
+
"globalOptions": [
|
|
4798
|
+
{
|
|
4799
|
+
"name": "--help",
|
|
4800
|
+
"description": "Show help for this command."
|
|
4801
|
+
},
|
|
4802
|
+
{
|
|
4803
|
+
"name": "--quiet",
|
|
4804
|
+
"description": "Suppress non-error diagnostics."
|
|
4805
|
+
},
|
|
4806
|
+
{
|
|
4807
|
+
"name": "--debug",
|
|
4808
|
+
"description": "Emit sanitized diagnostic traces."
|
|
4809
|
+
},
|
|
4810
|
+
{
|
|
4811
|
+
"name": "--api-url",
|
|
4812
|
+
"description": "Override the Budget Builder API base URL."
|
|
4813
|
+
},
|
|
4814
|
+
{
|
|
4815
|
+
"name": "--no-browser",
|
|
4816
|
+
"description": "Print the authorization URL without opening a browser."
|
|
4817
|
+
},
|
|
4818
|
+
{
|
|
4819
|
+
"name": "--allow-state-change",
|
|
4820
|
+
"description": "Allow a Budget Builder state change in non-interactive use."
|
|
4821
|
+
},
|
|
4822
|
+
{
|
|
4823
|
+
"name": "--allow-email",
|
|
4824
|
+
"description": "Allow sending email in non-interactive use."
|
|
4825
|
+
},
|
|
4826
|
+
{
|
|
4827
|
+
"name": "--allow-external-write",
|
|
4828
|
+
"description": "Allow writes to external systems in non-interactive use."
|
|
4829
|
+
},
|
|
4830
|
+
{
|
|
4831
|
+
"name": "--allow-delete",
|
|
4832
|
+
"description": "Allow deleting data in non-interactive use."
|
|
4833
|
+
},
|
|
4834
|
+
{
|
|
4835
|
+
"name": "--allow-financial-write",
|
|
4836
|
+
"description": "Allow financial-record changes in non-interactive use."
|
|
4837
|
+
},
|
|
4838
|
+
{
|
|
4839
|
+
"name": "--query",
|
|
4840
|
+
"description": "Company name to search in BB and QuickBooks."
|
|
4841
|
+
},
|
|
4842
|
+
{
|
|
4843
|
+
"name": "--page",
|
|
4844
|
+
"description": "Results page (eight companies per page)."
|
|
4845
|
+
}
|
|
4846
|
+
],
|
|
4847
|
+
"argumentMode": "legacy-passthrough",
|
|
4848
|
+
"effects": [],
|
|
4849
|
+
"legacyTarget": "search-existing-companies"
|
|
4850
|
+
},
|
|
4851
|
+
{
|
|
4852
|
+
"path": ["company", "use"],
|
|
4853
|
+
"legacyAliases": ["use-existing-company", "use_existing_company"],
|
|
4854
|
+
"summary": "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.",
|
|
4855
|
+
"globalOptions": [
|
|
4856
|
+
{
|
|
4857
|
+
"name": "--help",
|
|
4858
|
+
"description": "Show help for this command."
|
|
4859
|
+
},
|
|
4860
|
+
{
|
|
4861
|
+
"name": "--quiet",
|
|
4862
|
+
"description": "Suppress non-error diagnostics."
|
|
4863
|
+
},
|
|
4864
|
+
{
|
|
4865
|
+
"name": "--debug",
|
|
4866
|
+
"description": "Emit sanitized diagnostic traces."
|
|
4867
|
+
},
|
|
4868
|
+
{
|
|
4869
|
+
"name": "--api-url",
|
|
4870
|
+
"description": "Override the Budget Builder API base URL."
|
|
4871
|
+
},
|
|
4872
|
+
{
|
|
4873
|
+
"name": "--no-browser",
|
|
4874
|
+
"description": "Print the authorization URL without opening a browser."
|
|
4875
|
+
},
|
|
4876
|
+
{
|
|
4877
|
+
"name": "--allow-state-change",
|
|
4878
|
+
"description": "Allow a Budget Builder state change in non-interactive use."
|
|
4879
|
+
},
|
|
4880
|
+
{
|
|
4881
|
+
"name": "--allow-email",
|
|
4882
|
+
"description": "Allow sending email in non-interactive use."
|
|
4883
|
+
},
|
|
4884
|
+
{
|
|
4885
|
+
"name": "--allow-external-write",
|
|
4886
|
+
"description": "Allow writes to external systems in non-interactive use."
|
|
4887
|
+
},
|
|
4888
|
+
{
|
|
4889
|
+
"name": "--allow-delete",
|
|
4890
|
+
"description": "Allow deleting data in non-interactive use."
|
|
4891
|
+
},
|
|
4892
|
+
{
|
|
4893
|
+
"name": "--allow-financial-write",
|
|
4894
|
+
"description": "Allow financial-record changes in non-interactive use."
|
|
4895
|
+
},
|
|
4896
|
+
{
|
|
4897
|
+
"name": "--companyId",
|
|
4898
|
+
"description": "Existing BB company ID; combine with quickbooksId to explicitly connect both records."
|
|
4899
|
+
},
|
|
4900
|
+
{
|
|
4901
|
+
"name": "--quickbooksId",
|
|
4902
|
+
"description": "Existing QBO customer ID; alone imports or reuses its BB company."
|
|
4903
|
+
},
|
|
4904
|
+
{
|
|
4905
|
+
"name": "--website",
|
|
4906
|
+
"description": "Website only when missing from a QBO-only import."
|
|
4907
|
+
},
|
|
4908
|
+
{
|
|
4909
|
+
"name": "--confirmDistinct",
|
|
4910
|
+
"description": "Set true only when similar records do not represent this legal entity."
|
|
4911
|
+
}
|
|
4912
|
+
],
|
|
4913
|
+
"argumentMode": "legacy-passthrough",
|
|
4914
|
+
"effects": ["state-change", "external-write"],
|
|
4915
|
+
"legacyTarget": "use-existing-company"
|
|
4916
|
+
},
|
|
4783
4917
|
{
|
|
4784
4918
|
"path": ["company", "create"],
|
|
4785
4919
|
"legacyAliases": ["create-company", "create_company"],
|
|
4786
|
-
"summary": "Create
|
|
4920
|
+
"summary": "Create or reuse a company in BB and QuickBooks. --name <name> --website <url>. Similar names require --confirmDistinct true; inactive companies are never reactivated.",
|
|
4787
4921
|
"globalOptions": [
|
|
4788
4922
|
{
|
|
4789
4923
|
"name": "--help",
|
|
@@ -4824,6 +4958,10 @@
|
|
|
4824
4958
|
{
|
|
4825
4959
|
"name": "--allow-financial-write",
|
|
4826
4960
|
"description": "Allow financial-record changes in non-interactive use."
|
|
4961
|
+
},
|
|
4962
|
+
{
|
|
4963
|
+
"name": "--confirmDistinct",
|
|
4964
|
+
"description": "Set true to confirm a separate legal entity despite similar names."
|
|
4827
4965
|
}
|
|
4828
4966
|
],
|
|
4829
4967
|
"argumentMode": "legacy-passthrough",
|
package/command-reference.md
CHANGED
|
@@ -507,7 +507,7 @@ Effects: state-change, delete.
|
|
|
507
507
|
|
|
508
508
|
## `bb quotation create`
|
|
509
509
|
|
|
510
|
-
Create a quotation with an approved supplier.
|
|
510
|
+
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.
|
|
511
511
|
|
|
512
512
|
Legacy aliases: `create-quotation`, `create_quotation`.
|
|
513
513
|
|
|
@@ -515,7 +515,7 @@ Effects: state-change.
|
|
|
515
515
|
|
|
516
516
|
## `bb quotation update`
|
|
517
517
|
|
|
518
|
-
Update a quotation with
|
|
518
|
+
Update a DRAFT or REJECTED quotation using --payload JSON with id and the complete creation payload. Multiple budget items require lineAmounts: [{"budgetItemId":"<first-id>","amount":750},{"budgetItemId":"<second-id>","amount":450}], covering every budgetItemIds entry exactly once and summing to amountWithoutGst. Amounts exclude GST; use gstAmount: 0 for no GST.
|
|
519
519
|
|
|
520
520
|
Legacy aliases: `update-quotation`, `update_quotation`.
|
|
521
521
|
|
|
@@ -769,9 +769,25 @@ Legacy aliases: `get-company`, `get_company`.
|
|
|
769
769
|
|
|
770
770
|
Effects: none.
|
|
771
771
|
|
|
772
|
+
## `bb company search`
|
|
773
|
+
|
|
774
|
+
Search BB and QuickBooks companies without writes. --query <name> [--page <number>].
|
|
775
|
+
|
|
776
|
+
Legacy aliases: `search-existing-companies`, `search_existing_companies`.
|
|
777
|
+
|
|
778
|
+
Effects: none.
|
|
779
|
+
|
|
780
|
+
## `bb company use`
|
|
781
|
+
|
|
782
|
+
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.
|
|
783
|
+
|
|
784
|
+
Legacy aliases: `use-existing-company`, `use_existing_company`.
|
|
785
|
+
|
|
786
|
+
Effects: state-change, external-write.
|
|
787
|
+
|
|
772
788
|
## `bb company create`
|
|
773
789
|
|
|
774
|
-
Create
|
|
790
|
+
Create or reuse a company in BB and QuickBooks. --name <name> --website <url>. Similar names require --confirmDistinct true; inactive companies are never reactivated.
|
|
775
791
|
|
|
776
792
|
Legacy aliases: `create-company`, `create_company`.
|
|
777
793
|
|
package/dist/index.js
CHANGED
|
@@ -19478,14 +19478,6 @@ var logoutCliSession = async ({
|
|
|
19478
19478
|
// src/cli.ts
|
|
19479
19479
|
import { readFileSync } from "fs";
|
|
19480
19480
|
|
|
19481
|
-
// src/api-client.ts
|
|
19482
|
-
import {
|
|
19483
|
-
createTRPCProxyClient,
|
|
19484
|
-
httpBatchLink,
|
|
19485
|
-
loggerLink
|
|
19486
|
-
} from "@trpc/client";
|
|
19487
|
-
import SuperJSON from "superjson";
|
|
19488
|
-
|
|
19489
19481
|
// src/cli-trace.ts
|
|
19490
19482
|
var cliQuietSession = false;
|
|
19491
19483
|
var setCliQuiet = (quiet) => {
|
|
@@ -19570,6 +19562,12 @@ var normalizeApiBaseUrl = (value) => {
|
|
|
19570
19562
|
};
|
|
19571
19563
|
|
|
19572
19564
|
// src/api-client.ts
|
|
19565
|
+
import {
|
|
19566
|
+
createTRPCProxyClient,
|
|
19567
|
+
httpBatchLink,
|
|
19568
|
+
loggerLink
|
|
19569
|
+
} from "@trpc/client";
|
|
19570
|
+
import SuperJSON from "superjson";
|
|
19573
19571
|
var BUDGET_BUILDER_API_BASE_URL = DEFAULT_BUDGET_BUILDER_API_BASE_URL;
|
|
19574
19572
|
var interactiveAccessToken;
|
|
19575
19573
|
function getAuthHeader() {
|
|
@@ -20130,6 +20128,138 @@ var normalizePlainTextRichTextField = ({
|
|
|
20130
20128
|
};
|
|
20131
20129
|
};
|
|
20132
20130
|
|
|
20131
|
+
// src/runtime/error.ts
|
|
20132
|
+
var invocationErrorCodes = new Set([
|
|
20133
|
+
"USAGE",
|
|
20134
|
+
"VALIDATION",
|
|
20135
|
+
"CONFIRMATION_REQUIRED"
|
|
20136
|
+
]);
|
|
20137
|
+
var trpcCodeMap = {
|
|
20138
|
+
BAD_REQUEST: "VALIDATION",
|
|
20139
|
+
CONFLICT: "CONFLICT",
|
|
20140
|
+
FORBIDDEN: "AUTHORIZATION",
|
|
20141
|
+
INTERNAL_SERVER_ERROR: "API",
|
|
20142
|
+
METHOD_NOT_SUPPORTED: "API",
|
|
20143
|
+
NOT_FOUND: "NOT_FOUND",
|
|
20144
|
+
PARSE_ERROR: "VALIDATION",
|
|
20145
|
+
PAYLOAD_TOO_LARGE: "VALIDATION",
|
|
20146
|
+
PRECONDITION_FAILED: "CONFLICT",
|
|
20147
|
+
TIMEOUT: "NETWORK",
|
|
20148
|
+
TOO_MANY_REQUESTS: "API",
|
|
20149
|
+
UNAUTHORIZED: "AUTHENTICATION",
|
|
20150
|
+
UNPROCESSABLE_CONTENT: "VALIDATION"
|
|
20151
|
+
};
|
|
20152
|
+
var cliErrorCodes = new Set([
|
|
20153
|
+
"USAGE",
|
|
20154
|
+
"VALIDATION",
|
|
20155
|
+
"CONFIRMATION_REQUIRED",
|
|
20156
|
+
"AUTHENTICATION",
|
|
20157
|
+
"AUTHORIZATION",
|
|
20158
|
+
"NOT_FOUND",
|
|
20159
|
+
"CONFLICT",
|
|
20160
|
+
"API",
|
|
20161
|
+
"NETWORK",
|
|
20162
|
+
"ABORTED",
|
|
20163
|
+
"INTERRUPTED",
|
|
20164
|
+
"INTERNAL"
|
|
20165
|
+
]);
|
|
20166
|
+
|
|
20167
|
+
class CliRuntimeError extends Error {
|
|
20168
|
+
code;
|
|
20169
|
+
details;
|
|
20170
|
+
requestId;
|
|
20171
|
+
constructor(code, message, options = {}) {
|
|
20172
|
+
super(message);
|
|
20173
|
+
this.name = "CliRuntimeError";
|
|
20174
|
+
this.code = code;
|
|
20175
|
+
this.details = options.details;
|
|
20176
|
+
this.requestId = options.requestId;
|
|
20177
|
+
}
|
|
20178
|
+
}
|
|
20179
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20180
|
+
var asNonEmptyString = (value) => typeof value === "string" && value.trim() !== "" ? value : undefined;
|
|
20181
|
+
var asDetails = (value) => {
|
|
20182
|
+
if (!isRecord(value))
|
|
20183
|
+
return;
|
|
20184
|
+
const details = {};
|
|
20185
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
20186
|
+
if (isJsonValue(nestedValue))
|
|
20187
|
+
details[key] = nestedValue;
|
|
20188
|
+
}
|
|
20189
|
+
return Object.keys(details).length > 0 ? details : undefined;
|
|
20190
|
+
};
|
|
20191
|
+
var isJsonValue = (value) => {
|
|
20192
|
+
if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number") {
|
|
20193
|
+
return true;
|
|
20194
|
+
}
|
|
20195
|
+
if (Array.isArray(value))
|
|
20196
|
+
return value.every(isJsonValue);
|
|
20197
|
+
return isRecord(value) && Object.values(value).every(isJsonValue);
|
|
20198
|
+
};
|
|
20199
|
+
var mapErrorCode = (value) => {
|
|
20200
|
+
if (typeof value !== "string")
|
|
20201
|
+
return;
|
|
20202
|
+
if (cliErrorCodes.has(value))
|
|
20203
|
+
return value;
|
|
20204
|
+
if (value in trpcCodeMap)
|
|
20205
|
+
return trpcCodeMap[value];
|
|
20206
|
+
return;
|
|
20207
|
+
};
|
|
20208
|
+
var networkMessagePattern = /\b(fetch failed|network error|unable to connect|econnrefused|econnreset|enotfound|etimedout|socket hang up)\b/i;
|
|
20209
|
+
var isNetworkError = (error61, depth = 0) => {
|
|
20210
|
+
if (depth > 3 || !isRecord(error61))
|
|
20211
|
+
return false;
|
|
20212
|
+
const message = asNonEmptyString(error61.message);
|
|
20213
|
+
if (message !== undefined && networkMessagePattern.test(message))
|
|
20214
|
+
return true;
|
|
20215
|
+
const code = asNonEmptyString(error61.code);
|
|
20216
|
+
if (code !== undefined && networkMessagePattern.test(code))
|
|
20217
|
+
return true;
|
|
20218
|
+
return isNetworkError(error61.cause, depth + 1);
|
|
20219
|
+
};
|
|
20220
|
+
var normalizeCliError = (error61) => {
|
|
20221
|
+
if (error61 instanceof CliRuntimeError) {
|
|
20222
|
+
return {
|
|
20223
|
+
code: error61.code,
|
|
20224
|
+
message: error61.message,
|
|
20225
|
+
...error61.details === undefined ? {} : { details: error61.details },
|
|
20226
|
+
...error61.requestId === undefined ? {} : { requestId: error61.requestId }
|
|
20227
|
+
};
|
|
20228
|
+
}
|
|
20229
|
+
if (isNetworkError(error61)) {
|
|
20230
|
+
return {
|
|
20231
|
+
code: "NETWORK",
|
|
20232
|
+
message: error61 instanceof Error ? error61.message : "The Budget Builder API could not be reached."
|
|
20233
|
+
};
|
|
20234
|
+
}
|
|
20235
|
+
if (isRecord(error61)) {
|
|
20236
|
+
const data = isRecord(error61.data) ? error61.data : undefined;
|
|
20237
|
+
const transportCode = mapErrorCode(data?.code ?? error61.code);
|
|
20238
|
+
if (error61 instanceof Error && data === undefined && transportCode === undefined) {
|
|
20239
|
+
return { code: "INTERNAL", message: error61.message };
|
|
20240
|
+
}
|
|
20241
|
+
const code = transportCode ?? "API";
|
|
20242
|
+
const message = asNonEmptyString(error61.message) ?? "The Budget Builder API request failed.";
|
|
20243
|
+
const requestId = asNonEmptyString(data?.requestId) ?? asNonEmptyString(data?.requestID) ?? asNonEmptyString(error61.requestId);
|
|
20244
|
+
const details = asDetails(data?.details ?? error61.details);
|
|
20245
|
+
return {
|
|
20246
|
+
code,
|
|
20247
|
+
message,
|
|
20248
|
+
...details === undefined ? {} : { details },
|
|
20249
|
+
...requestId === undefined ? {} : { requestId }
|
|
20250
|
+
};
|
|
20251
|
+
}
|
|
20252
|
+
if (error61 instanceof Error) {
|
|
20253
|
+
return { code: "INTERNAL", message: error61.message };
|
|
20254
|
+
}
|
|
20255
|
+
return { code: "INTERNAL", message: "An unexpected CLI error occurred." };
|
|
20256
|
+
};
|
|
20257
|
+
var exitCodeForCliError = (error61) => {
|
|
20258
|
+
if (error61.code === "INTERRUPTED")
|
|
20259
|
+
return 130;
|
|
20260
|
+
return invocationErrorCodes.has(error61.code) ? 2 : 1;
|
|
20261
|
+
};
|
|
20262
|
+
|
|
20133
20263
|
// src/parse-mutation-payload.ts
|
|
20134
20264
|
function requireObject(raw, label) {
|
|
20135
20265
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
@@ -20547,12 +20677,42 @@ function parseUpdateBillPaymentEvidencePayload(raw) {
|
|
|
20547
20677
|
requireObject(raw, "update-bill-payment-evidence payload");
|
|
20548
20678
|
return raw;
|
|
20549
20679
|
}
|
|
20680
|
+
var validateQuotationAllocations = (raw, command) => {
|
|
20681
|
+
const input2 = requireObject(raw, `${command} payload`);
|
|
20682
|
+
const ids = input2.budgetItemIds;
|
|
20683
|
+
if (!Array.isArray(ids) || ids.length === 0 || ids.some((id) => typeof id !== "string" || id.length === 0) || new Set(ids).size !== ids.length) {
|
|
20684
|
+
throw new CliRuntimeError("VALIDATION", `${command} payload.budgetItemIds must contain each selected budget-item ID exactly once.`);
|
|
20685
|
+
}
|
|
20686
|
+
const isCurrency = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 99999999.99 && Math.abs(value * 100 - Math.round(value * 100)) < 0.00000001;
|
|
20687
|
+
if (!isCurrency(input2.amountWithoutGst) || input2.amountWithoutGst === 0 || !isCurrency(input2.gstAmount)) {
|
|
20688
|
+
throw new CliRuntimeError("VALIDATION", `${command} requires a positive payload.amountWithoutGst and a non-negative payload.gstAmount, in currency units with at most two decimal places. Use gstAmount: 0 for no GST.`);
|
|
20689
|
+
}
|
|
20690
|
+
const guidance = `${command} requires payload.lineAmounts as [{"budgetItemId":"<first-id>","amount":750},{"budgetItemId":"<second-id>","amount":450}] for multiple budget items. Include each payload.budgetItemIds entry exactly once, with non-negative amounts before GST summing to payload.amountWithoutGst. For a 1200 quotation with no GST, use line amounts 750 and 450, amountWithoutGst: 1200, gstAmount: 0.`;
|
|
20691
|
+
if (input2.lineAmounts === undefined && ids.length === 1)
|
|
20692
|
+
return;
|
|
20693
|
+
if (!Array.isArray(input2.lineAmounts) || input2.lineAmounts.length !== ids.length) {
|
|
20694
|
+
throw new CliRuntimeError("VALIDATION", guidance);
|
|
20695
|
+
}
|
|
20696
|
+
const seen = new Set;
|
|
20697
|
+
let totalCents = 0;
|
|
20698
|
+
for (const rawLine of input2.lineAmounts) {
|
|
20699
|
+
const line = requireObject(rawLine, `${command} payload.lineAmounts entry`);
|
|
20700
|
+
if (typeof line.budgetItemId !== "string" || !ids.includes(line.budgetItemId) || seen.has(line.budgetItemId) || !isCurrency(line.amount)) {
|
|
20701
|
+
throw new CliRuntimeError("VALIDATION", guidance);
|
|
20702
|
+
}
|
|
20703
|
+
seen.add(line.budgetItemId);
|
|
20704
|
+
totalCents += Math.round(line.amount * 100);
|
|
20705
|
+
}
|
|
20706
|
+
if (totalCents !== Math.round(input2.amountWithoutGst * 100)) {
|
|
20707
|
+
throw new CliRuntimeError("VALIDATION", guidance);
|
|
20708
|
+
}
|
|
20709
|
+
};
|
|
20550
20710
|
function parseCreateQuotationPayload(raw) {
|
|
20551
|
-
|
|
20711
|
+
validateQuotationAllocations(raw, "create-quotation");
|
|
20552
20712
|
return raw;
|
|
20553
20713
|
}
|
|
20554
20714
|
function parseUpdateQuotationPayload(raw) {
|
|
20555
|
-
|
|
20715
|
+
validateQuotationAllocations(raw, "update-quotation");
|
|
20556
20716
|
return raw;
|
|
20557
20717
|
}
|
|
20558
20718
|
function parseCreateCustomerInvoicePayload(raw) {
|
|
@@ -20663,138 +20823,6 @@ function parseCompanyUpdatePayload(raw) {
|
|
|
20663
20823
|
return raw;
|
|
20664
20824
|
}
|
|
20665
20825
|
|
|
20666
|
-
// src/runtime/error.ts
|
|
20667
|
-
var invocationErrorCodes = new Set([
|
|
20668
|
-
"USAGE",
|
|
20669
|
-
"VALIDATION",
|
|
20670
|
-
"CONFIRMATION_REQUIRED"
|
|
20671
|
-
]);
|
|
20672
|
-
var trpcCodeMap = {
|
|
20673
|
-
BAD_REQUEST: "VALIDATION",
|
|
20674
|
-
CONFLICT: "CONFLICT",
|
|
20675
|
-
FORBIDDEN: "AUTHORIZATION",
|
|
20676
|
-
INTERNAL_SERVER_ERROR: "API",
|
|
20677
|
-
METHOD_NOT_SUPPORTED: "API",
|
|
20678
|
-
NOT_FOUND: "NOT_FOUND",
|
|
20679
|
-
PARSE_ERROR: "VALIDATION",
|
|
20680
|
-
PAYLOAD_TOO_LARGE: "VALIDATION",
|
|
20681
|
-
PRECONDITION_FAILED: "CONFLICT",
|
|
20682
|
-
TIMEOUT: "NETWORK",
|
|
20683
|
-
TOO_MANY_REQUESTS: "API",
|
|
20684
|
-
UNAUTHORIZED: "AUTHENTICATION",
|
|
20685
|
-
UNPROCESSABLE_CONTENT: "VALIDATION"
|
|
20686
|
-
};
|
|
20687
|
-
var cliErrorCodes = new Set([
|
|
20688
|
-
"USAGE",
|
|
20689
|
-
"VALIDATION",
|
|
20690
|
-
"CONFIRMATION_REQUIRED",
|
|
20691
|
-
"AUTHENTICATION",
|
|
20692
|
-
"AUTHORIZATION",
|
|
20693
|
-
"NOT_FOUND",
|
|
20694
|
-
"CONFLICT",
|
|
20695
|
-
"API",
|
|
20696
|
-
"NETWORK",
|
|
20697
|
-
"ABORTED",
|
|
20698
|
-
"INTERRUPTED",
|
|
20699
|
-
"INTERNAL"
|
|
20700
|
-
]);
|
|
20701
|
-
|
|
20702
|
-
class CliRuntimeError extends Error {
|
|
20703
|
-
code;
|
|
20704
|
-
details;
|
|
20705
|
-
requestId;
|
|
20706
|
-
constructor(code, message, options = {}) {
|
|
20707
|
-
super(message);
|
|
20708
|
-
this.name = "CliRuntimeError";
|
|
20709
|
-
this.code = code;
|
|
20710
|
-
this.details = options.details;
|
|
20711
|
-
this.requestId = options.requestId;
|
|
20712
|
-
}
|
|
20713
|
-
}
|
|
20714
|
-
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20715
|
-
var asNonEmptyString = (value) => typeof value === "string" && value.trim() !== "" ? value : undefined;
|
|
20716
|
-
var asDetails = (value) => {
|
|
20717
|
-
if (!isRecord(value))
|
|
20718
|
-
return;
|
|
20719
|
-
const details = {};
|
|
20720
|
-
for (const [key, nestedValue] of Object.entries(value)) {
|
|
20721
|
-
if (isJsonValue(nestedValue))
|
|
20722
|
-
details[key] = nestedValue;
|
|
20723
|
-
}
|
|
20724
|
-
return Object.keys(details).length > 0 ? details : undefined;
|
|
20725
|
-
};
|
|
20726
|
-
var isJsonValue = (value) => {
|
|
20727
|
-
if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number") {
|
|
20728
|
-
return true;
|
|
20729
|
-
}
|
|
20730
|
-
if (Array.isArray(value))
|
|
20731
|
-
return value.every(isJsonValue);
|
|
20732
|
-
return isRecord(value) && Object.values(value).every(isJsonValue);
|
|
20733
|
-
};
|
|
20734
|
-
var mapErrorCode = (value) => {
|
|
20735
|
-
if (typeof value !== "string")
|
|
20736
|
-
return;
|
|
20737
|
-
if (cliErrorCodes.has(value))
|
|
20738
|
-
return value;
|
|
20739
|
-
if (value in trpcCodeMap)
|
|
20740
|
-
return trpcCodeMap[value];
|
|
20741
|
-
return;
|
|
20742
|
-
};
|
|
20743
|
-
var networkMessagePattern = /\b(fetch failed|network error|unable to connect|econnrefused|econnreset|enotfound|etimedout|socket hang up)\b/i;
|
|
20744
|
-
var isNetworkError = (error61, depth = 0) => {
|
|
20745
|
-
if (depth > 3 || !isRecord(error61))
|
|
20746
|
-
return false;
|
|
20747
|
-
const message = asNonEmptyString(error61.message);
|
|
20748
|
-
if (message !== undefined && networkMessagePattern.test(message))
|
|
20749
|
-
return true;
|
|
20750
|
-
const code = asNonEmptyString(error61.code);
|
|
20751
|
-
if (code !== undefined && networkMessagePattern.test(code))
|
|
20752
|
-
return true;
|
|
20753
|
-
return isNetworkError(error61.cause, depth + 1);
|
|
20754
|
-
};
|
|
20755
|
-
var normalizeCliError = (error61) => {
|
|
20756
|
-
if (error61 instanceof CliRuntimeError) {
|
|
20757
|
-
return {
|
|
20758
|
-
code: error61.code,
|
|
20759
|
-
message: error61.message,
|
|
20760
|
-
...error61.details === undefined ? {} : { details: error61.details },
|
|
20761
|
-
...error61.requestId === undefined ? {} : { requestId: error61.requestId }
|
|
20762
|
-
};
|
|
20763
|
-
}
|
|
20764
|
-
if (isNetworkError(error61)) {
|
|
20765
|
-
return {
|
|
20766
|
-
code: "NETWORK",
|
|
20767
|
-
message: error61 instanceof Error ? error61.message : "The Budget Builder API could not be reached."
|
|
20768
|
-
};
|
|
20769
|
-
}
|
|
20770
|
-
if (isRecord(error61)) {
|
|
20771
|
-
const data = isRecord(error61.data) ? error61.data : undefined;
|
|
20772
|
-
const transportCode = mapErrorCode(data?.code ?? error61.code);
|
|
20773
|
-
if (error61 instanceof Error && data === undefined && transportCode === undefined) {
|
|
20774
|
-
return { code: "INTERNAL", message: error61.message };
|
|
20775
|
-
}
|
|
20776
|
-
const code = transportCode ?? "API";
|
|
20777
|
-
const message = asNonEmptyString(error61.message) ?? "The Budget Builder API request failed.";
|
|
20778
|
-
const requestId = asNonEmptyString(data?.requestId) ?? asNonEmptyString(data?.requestID) ?? asNonEmptyString(error61.requestId);
|
|
20779
|
-
const details = asDetails(data?.details ?? error61.details);
|
|
20780
|
-
return {
|
|
20781
|
-
code,
|
|
20782
|
-
message,
|
|
20783
|
-
...details === undefined ? {} : { details },
|
|
20784
|
-
...requestId === undefined ? {} : { requestId }
|
|
20785
|
-
};
|
|
20786
|
-
}
|
|
20787
|
-
if (error61 instanceof Error) {
|
|
20788
|
-
return { code: "INTERNAL", message: error61.message };
|
|
20789
|
-
}
|
|
20790
|
-
return { code: "INTERNAL", message: "An unexpected CLI error occurred." };
|
|
20791
|
-
};
|
|
20792
|
-
var exitCodeForCliError = (error61) => {
|
|
20793
|
-
if (error61.code === "INTERRUPTED")
|
|
20794
|
-
return 130;
|
|
20795
|
-
return invocationErrorCodes.has(error61.code) ? 2 : 1;
|
|
20796
|
-
};
|
|
20797
|
-
|
|
20798
20826
|
// src/runtime/confirmation.ts
|
|
20799
20827
|
var effectLabels = {
|
|
20800
20828
|
"state-change": "changes Budget Builder state",
|
|
@@ -22790,6 +22818,12 @@ async function createCompany(opts) {
|
|
|
22790
22818
|
const result = await api2.company.createCompany.mutate(opts);
|
|
22791
22819
|
out(result);
|
|
22792
22820
|
}
|
|
22821
|
+
var searchExistingCompanies = async (query, page = 1) => {
|
|
22822
|
+
out(await api2.company.searchExistingCompanies.query({ query, page }));
|
|
22823
|
+
};
|
|
22824
|
+
var reuseExistingCompany = async (input2) => {
|
|
22825
|
+
out(await api2.company.useExistingCompany.mutate(input2));
|
|
22826
|
+
};
|
|
22793
22827
|
async function listProjects(opts) {
|
|
22794
22828
|
const result = await api2.project.getAllProjects.query({
|
|
22795
22829
|
companyId: opts.companyId,
|
|
@@ -23308,6 +23342,7 @@ var mutationPrefixes = [
|
|
|
23308
23342
|
"submit-",
|
|
23309
23343
|
"sync-",
|
|
23310
23344
|
"update-",
|
|
23345
|
+
"use-existing-",
|
|
23311
23346
|
"upload-",
|
|
23312
23347
|
"void-"
|
|
23313
23348
|
];
|
|
@@ -23338,6 +23373,7 @@ var externalWriteTargets = new Set([
|
|
|
23338
23373
|
"complete-project",
|
|
23339
23374
|
"create-bill",
|
|
23340
23375
|
"create-company",
|
|
23376
|
+
"use-existing-company",
|
|
23341
23377
|
"create-estimate",
|
|
23342
23378
|
"create-placeholder-bill",
|
|
23343
23379
|
"create-customer-invoice",
|
|
@@ -23435,6 +23471,12 @@ var effectsFor = (legacyTarget) => {
|
|
|
23435
23471
|
};
|
|
23436
23472
|
var titleCase = (value) => value.split("-").map((word) => `${word.slice(0, 1).toUpperCase()}${word.slice(1)}`).join(" ");
|
|
23437
23473
|
var summaryFor = (legacyTarget) => {
|
|
23474
|
+
if (legacyTarget === "search-existing-companies")
|
|
23475
|
+
return "Search BB and QuickBooks companies without writes. --query <name> [--page <number>].";
|
|
23476
|
+
if (legacyTarget === "use-existing-company")
|
|
23477
|
+
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.";
|
|
23478
|
+
if (legacyTarget === "create-company")
|
|
23479
|
+
return "Create or reuse a company in BB and QuickBooks. --name <name> --website <url>. Similar names require --confirmDistinct true; inactive companies are never reactivated.";
|
|
23438
23480
|
if (legacyTarget === "preview-qbo-customer-invoice") {
|
|
23439
23481
|
return "Preview an existing QuickBooks invoice for a BB budget using read-only QBO access. Arguments: <budgetId> <qboInvoiceId>.";
|
|
23440
23482
|
}
|
|
@@ -23453,10 +23495,10 @@ var summaryFor = (legacyTarget) => {
|
|
|
23453
23495
|
return "Validate the approved supplier and selected bill line items.";
|
|
23454
23496
|
}
|
|
23455
23497
|
if (legacyTarget === "create-quotation") {
|
|
23456
|
-
return
|
|
23498
|
+
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.';
|
|
23457
23499
|
}
|
|
23458
23500
|
if (legacyTarget === "update-quotation") {
|
|
23459
|
-
return
|
|
23501
|
+
return 'Update a DRAFT or REJECTED quotation using --payload JSON with id and the complete creation payload. Multiple budget items require lineAmounts: [{"budgetItemId":"<first-id>","amount":750},{"budgetItemId":"<second-id>","amount":450}], covering every budgetItemIds entry exactly once and summing to amountWithoutGst. Amounts exclude GST; use gstAmount: 0 for no GST.';
|
|
23460
23502
|
}
|
|
23461
23503
|
if (legacyTarget === "download-customer-invoice-pdf") {
|
|
23462
23504
|
return "Download a customer invoice PDF when permitted.";
|
|
@@ -23628,8 +23670,20 @@ var registry2 = [
|
|
|
23628
23670
|
"staged",
|
|
23629
23671
|
"cleanup"
|
|
23630
23672
|
]),
|
|
23631
|
-
legacyCommand("create-quotation", ["quotation", "create"]
|
|
23632
|
-
|
|
23673
|
+
legacyCommand("create-quotation", ["quotation", "create"], [], [
|
|
23674
|
+
{
|
|
23675
|
+
name: "--payload",
|
|
23676
|
+
description: "Quotation JSON including lineAmounts for multiple selected budget items; see the payload format above.",
|
|
23677
|
+
required: true
|
|
23678
|
+
}
|
|
23679
|
+
]),
|
|
23680
|
+
legacyCommand("update-quotation", ["quotation", "update"], [], [
|
|
23681
|
+
{
|
|
23682
|
+
name: "--payload",
|
|
23683
|
+
description: "Complete quotation JSON plus id, including lineAmounts for multiple selected budget items.",
|
|
23684
|
+
required: true
|
|
23685
|
+
}
|
|
23686
|
+
]),
|
|
23633
23687
|
legacyCommand("delete-quotation", ["quotation", "delete"]),
|
|
23634
23688
|
legacyCommand("submit-quotation", ["quotation", "submit"]),
|
|
23635
23689
|
legacyCommand("approve-quotation", ["quotation", "approve"]),
|
|
@@ -23686,7 +23740,40 @@ var registry2 = [
|
|
|
23686
23740
|
legacyCommand("get-supplier-details", ["supplier", "detail", "get"]),
|
|
23687
23741
|
legacyCommand("list-companies", ["company", "list"]),
|
|
23688
23742
|
legacyCommand("get-company", ["company", "get"]),
|
|
23689
|
-
legacyCommand("
|
|
23743
|
+
legacyCommand("search-existing-companies", ["company", "search"], [], [
|
|
23744
|
+
{
|
|
23745
|
+
name: "--query",
|
|
23746
|
+
description: "Company name to search in BB and QuickBooks."
|
|
23747
|
+
},
|
|
23748
|
+
{
|
|
23749
|
+
name: "--page",
|
|
23750
|
+
description: "Results page (eight companies per page)."
|
|
23751
|
+
}
|
|
23752
|
+
]),
|
|
23753
|
+
legacyCommand("use-existing-company", ["company", "use"], [], [
|
|
23754
|
+
{
|
|
23755
|
+
name: "--companyId",
|
|
23756
|
+
description: "Existing BB company ID; combine with quickbooksId to explicitly connect both records."
|
|
23757
|
+
},
|
|
23758
|
+
{
|
|
23759
|
+
name: "--quickbooksId",
|
|
23760
|
+
description: "Existing QBO customer ID; alone imports or reuses its BB company."
|
|
23761
|
+
},
|
|
23762
|
+
{
|
|
23763
|
+
name: "--website",
|
|
23764
|
+
description: "Website only when missing from a QBO-only import."
|
|
23765
|
+
},
|
|
23766
|
+
{
|
|
23767
|
+
name: "--confirmDistinct",
|
|
23768
|
+
description: "Set true only when similar records do not represent this legal entity."
|
|
23769
|
+
}
|
|
23770
|
+
]),
|
|
23771
|
+
legacyCommand("create-company", ["company", "create"], [], [
|
|
23772
|
+
{
|
|
23773
|
+
name: "--confirmDistinct",
|
|
23774
|
+
description: "Set true to confirm a separate legal entity despite similar names."
|
|
23775
|
+
}
|
|
23776
|
+
]),
|
|
23690
23777
|
legacyCommand("update-company", ["company", "update"]),
|
|
23691
23778
|
legacyCommand("delete-company", ["company", "delete"]),
|
|
23692
23779
|
legacyCommand("list-projects", ["project", "list"]),
|
|
@@ -23877,12 +23964,28 @@ var resolveCommand = (argv) => {
|
|
|
23877
23964
|
isLegacyAlias: true
|
|
23878
23965
|
};
|
|
23879
23966
|
};
|
|
23880
|
-
var createHumanHelp = () => {
|
|
23881
|
-
|
|
23882
|
-
|
|
23883
|
-
|
|
23967
|
+
var createHumanHelp = (command) => {
|
|
23968
|
+
if (command) {
|
|
23969
|
+
return [
|
|
23970
|
+
`Usage: bb ${command.path.join(" ")} [options] [args]`,
|
|
23971
|
+
"",
|
|
23972
|
+
command.summary,
|
|
23973
|
+
"",
|
|
23974
|
+
`Legacy aliases: ${command.legacyAliases.join(", ")}`,
|
|
23975
|
+
"",
|
|
23976
|
+
"Options",
|
|
23977
|
+
...command.options.map((option) => ` ${option.name}
|
|
23978
|
+
${option.description}`),
|
|
23979
|
+
"",
|
|
23980
|
+
"Use `bb help --legacy` for additional command arguments."
|
|
23981
|
+
].join(`
|
|
23982
|
+
`);
|
|
23983
|
+
}
|
|
23984
|
+
const lines = commandRegistry.map((command2) => {
|
|
23985
|
+
const canonical = `bb ${command2.path.join(" ")}`;
|
|
23986
|
+
const aliases = command2.legacyAliases.filter((alias) => alias.includes("-")).map((alias) => `bb ${alias}`).join(", ");
|
|
23884
23987
|
return ` ${canonical}
|
|
23885
|
-
${
|
|
23988
|
+
${command2.summary}${aliases ? ` Legacy: ${aliases}.` : ""}`;
|
|
23886
23989
|
});
|
|
23887
23990
|
return [
|
|
23888
23991
|
"bb \u2014 Budget Builder CLI",
|
|
@@ -24167,8 +24270,8 @@ Quotations
|
|
|
24167
24270
|
get-quotation <quotationId>
|
|
24168
24271
|
upload-quotation-attachment <projectId> <filePath> Upload one PDF, JPEG, or PNG up to 20MB; returns attachment JSON for a quotation payload.
|
|
24169
24272
|
cleanup-staged-quotation-attachments <projectId> --keys <csv> Delete unattached staged quotation uploads.
|
|
24170
|
-
create-quotation --payload '<json>' (quotation.createDraft; requires amountWithoutGst and
|
|
24171
|
-
update-quotation --payload '<json>' Update a DRAFT or REJECTED quotation; same payload plus id.
|
|
24273
|
+
create-quotation --payload '<json>' (quotation.createDraft; requires amountWithoutGst, gstAmount, and lineAmounts [{budgetItemId, amount}] for multiple budgetItemIds; amounts before GST must sum to amountWithoutGst)
|
|
24274
|
+
update-quotation --payload '<json>' Update a DRAFT or REJECTED quotation; same allocation payload plus id; use lineAmounts [{budgetItemId, amount}] for every selected item.
|
|
24172
24275
|
delete-quotation <quotationId>
|
|
24173
24276
|
submit-quotation <quotationId> [--overBudgetOverrideReason <text>]
|
|
24174
24277
|
approve-quotation <quotationId> [--overBudgetOverrideReason <text>]
|
|
@@ -24207,7 +24310,12 @@ Companies & projects
|
|
|
24207
24310
|
list-companies [--name] [--createdBy] [--active true|false] [--page] [--perPage]
|
|
24208
24311
|
Omit --active to include both active and inactive companies.
|
|
24209
24312
|
get-company <id>
|
|
24210
|
-
|
|
24313
|
+
search-existing-companies --query <name> [--page] (BB + QuickBooks, read-only)
|
|
24314
|
+
use-existing-company --companyId <bbId> and/or --quickbooksId <qboId> [--website <url>] [--confirmDistinct true]
|
|
24315
|
+
Supply both IDs to explicitly connect existing records with different names.
|
|
24316
|
+
Reuses both records, creating only the missing counterpart. Website is needed only for a QBO import without one.
|
|
24317
|
+
create-company --name --website <url> [--confirmDistinct true]
|
|
24318
|
+
Exact existing companies are reused. Similar names require explicit separate-entity confirmation.
|
|
24211
24319
|
update-company --payload '<json>' (company.updateCompany; must include id, name, website)
|
|
24212
24320
|
delete-company <id>
|
|
24213
24321
|
list-projects [--companyId] [--name] [--status] [--active true|false] [--page] [--perPage]
|
|
@@ -24443,7 +24551,9 @@ async function runCli(argv = process.argv, runtime2 = createProcessRuntime(), cr
|
|
|
24443
24551
|
if (commandArgv.includes("--legacy")) {
|
|
24444
24552
|
printLegacyHelp(runtime2);
|
|
24445
24553
|
} else {
|
|
24446
|
-
|
|
24554
|
+
const helpArgv = commandArgv.filter((argument, index) => argument !== "-h" && argument !== "--help" && !argument.startsWith("--help=") && !(commandArgv[index - 1] === "--help" && booleanFlagValues.has(argument)));
|
|
24555
|
+
const helpCommand = resolveCommand(helpArgv[0] === "help" ? helpArgv.slice(1) : helpArgv)?.command;
|
|
24556
|
+
runtime2.stdout.write(`${createHumanHelp(helpCommand)}
|
|
24447
24557
|
`);
|
|
24448
24558
|
}
|
|
24449
24559
|
return 0;
|
|
@@ -25593,7 +25703,39 @@ Waiting for approval\u2026
|
|
|
25593
25703
|
if (!name || !website) {
|
|
25594
25704
|
throw new Error("create-company requires --name and --website <url>");
|
|
25595
25705
|
}
|
|
25596
|
-
await createCompany({
|
|
25706
|
+
await createCompany({
|
|
25707
|
+
name,
|
|
25708
|
+
website,
|
|
25709
|
+
confirmDistinct: parseOptionalBoolFlag(flags, "confirmDistinct")
|
|
25710
|
+
});
|
|
25711
|
+
break;
|
|
25712
|
+
}
|
|
25713
|
+
case "search-existing-companies": {
|
|
25714
|
+
const query = getFlag(flags, "query");
|
|
25715
|
+
if (!query)
|
|
25716
|
+
throw new Error("search-existing-companies requires --query <name>");
|
|
25717
|
+
await searchExistingCompanies(query, parsePositiveIntFlag(getFlag(flags, "page"), "--page"));
|
|
25718
|
+
break;
|
|
25719
|
+
}
|
|
25720
|
+
case "use-existing-company": {
|
|
25721
|
+
const companyId = getFlag(flags, "companyId");
|
|
25722
|
+
const quickbooksId = getFlag(flags, "quickbooksId");
|
|
25723
|
+
if (!companyId && !quickbooksId)
|
|
25724
|
+
throw new Error("Supply --companyId and/or --quickbooksId");
|
|
25725
|
+
if (companyId)
|
|
25726
|
+
await reuseExistingCompany({
|
|
25727
|
+
source: "bb",
|
|
25728
|
+
companyId,
|
|
25729
|
+
quickbooksId,
|
|
25730
|
+
confirmDistinct: parseOptionalBoolFlag(flags, "confirmDistinct")
|
|
25731
|
+
});
|
|
25732
|
+
else if (quickbooksId)
|
|
25733
|
+
await reuseExistingCompany({
|
|
25734
|
+
source: "qbo",
|
|
25735
|
+
quickbooksId,
|
|
25736
|
+
website: getFlag(flags, "website"),
|
|
25737
|
+
confirmDistinct: parseOptionalBoolFlag(flags, "confirmDistinct")
|
|
25738
|
+
});
|
|
25597
25739
|
break;
|
|
25598
25740
|
}
|
|
25599
25741
|
case "update-company": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@go-labs-sg/bb",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.34.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",
|
|
@@ -134,6 +134,46 @@ The following distinctions are especially important:
|
|
|
134
134
|
- Ordinary unpaid creation does not require `--allow-email`, but remains role-dependent: Accounting auto-checks and Admin auto-approves. It still requires `--allow-external-write`, including for possible placeholder adjustments. Already-paid creation retains the email gate.
|
|
135
135
|
- Staged attachments belong to the user in the database. Do not reuse, share, or manually construct staged keys.
|
|
136
136
|
|
|
137
|
+
## Creating and updating quotations
|
|
138
|
+
|
|
139
|
+
Quotation payloads keep the supplier subtotal and GST in separate fields: `amountWithoutGst` is the subtotal and `gstAmount` is GST. For a multi-line quotation, `lineAmounts` is required and must list every selected budget item exactly once; its amounts must add up to `amountWithoutGst`. Read the budget first and copy the actual `budgetItemIds` returned by Budget Builder. Never substitute item names, invented IDs, or IDs from another budget or project.
|
|
140
|
+
|
|
141
|
+
`quotationAllocations` is a bill-side source allocation. Use it when creating or updating a supplier bill to record which approved quotations fund bill lines. Do not put `quotationAllocations` in a quotation create/update payload.
|
|
142
|
+
|
|
143
|
+
Upload one quotation file with `bb quotation attachment upload <project-id> <file-path>`, then pass the one returned attachment object in the quotation payload's one-element `attachments` array. The create and update procedures require that single original quotation attachment.
|
|
144
|
+
|
|
145
|
+
Replace the example attachment size `12345` with the actual byte count from the upload response.
|
|
146
|
+
|
|
147
|
+
Use this complete placeholder shape for a quotation with S$750 on one real budget line and S$450 on another (S$1,200 subtotal, S$0 GST):
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
bb quotation attachment upload "<project-id>" "<quotation-file.pdf>"
|
|
151
|
+
|
|
152
|
+
# Replace every <...> placeholder with values from the budget read or upload response.
|
|
153
|
+
bb quotation create --payload '{
|
|
154
|
+
"projectId": "<project-id>",
|
|
155
|
+
"budgetId": "<budget-id>",
|
|
156
|
+
"supplierId": "<supplier-id>",
|
|
157
|
+
"budgetItemIds": ["<budget-item-id-for-750>", "<budget-item-id-for-450>"],
|
|
158
|
+
"lineAmounts": [
|
|
159
|
+
{"budgetItemId": "<budget-item-id-for-750>", "amount": 750},
|
|
160
|
+
{"budgetItemId": "<budget-item-id-for-450>", "amount": 450}
|
|
161
|
+
],
|
|
162
|
+
"amountWithoutGst": 1200,
|
|
163
|
+
"gstAmount": 0,
|
|
164
|
+
"attachments": [
|
|
165
|
+
{
|
|
166
|
+
"id": "<attachment-id-from-upload>",
|
|
167
|
+
"name": "<attachment-name-from-upload>",
|
|
168
|
+
"key": "<attachment-key-from-upload>",
|
|
169
|
+
"size": 12345
|
|
170
|
+
}
|
|
171
|
+
]
|
|
172
|
+
}'
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
For an update, use the same shape with `"id": "<quotation-id>"` and the existing quotation's editable status. The numeric example is documentation-only; it contains no live resource IDs. Report the quotation subtotal, GST, and total separately after reading the result.
|
|
176
|
+
|
|
137
177
|
## Bill amounts: always identify the subtotal before writing
|
|
138
178
|
|
|
139
179
|
For bill creation (including `--draft-only`) and updates, `amount`, `extractedAmount`, `lineAmounts[].amount`, and `quotationAllocations[].amount` all **exclude GST**. The CLI does not strip GST from a supplied total. BB calculates applicable GST separately from the supplier and line settings.
|