@go-labs-sg/bb 2.31.0 → 2.32.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 +50 -4
- package/command-manifest.json +176 -0
- package/command-reference.md +24 -0
- package/dist/index.js +321 -18
- package/package.json +1 -1
- package/role-aware-agent-guide.md +1 -0
package/README.md
CHANGED
|
@@ -232,6 +232,18 @@ Every canonical command is classified by effect: `state-change`, `email`, `exter
|
|
|
232
232
|
|
|
233
233
|
**Draft-only bills:** Use `bb bill create --draft-only --payload '<bill-json>' --allow-state-change --allow-financial-write` when the user authorizes a draft only. The equivalent payload field is `draftOnly: true`. This creates `DRAFT` for every role, without auto-checking, auto-approval, approval requests, emails, or QuickBooks writes. It cannot be combined with `alreadyPaid: true`; conflicting flag and payload values are rejected. Draft-only creation uses the dedicated `bill.createDraft` API route and fails on older servers without falling back to ordinary creation. Deploy backend support before releasing the updated CLI. Reconciled projects must be moved to Won separately before draft-only creation, because reopening a project can trigger external integrations. Completed projects retain their existing creation restriction.
|
|
234
234
|
|
|
235
|
+
**Quotation-first supplier bills:** Use `quotationIds` when approved quotations cover disjoint bill lines. When two quotations fund the same bill line, pass `quotationAllocations` entries with `{ budgetItemId, quotationId, amount }` and an exact `lineAmounts` entry for every selected budget item; `lineAmounts` must sum to the bill amount. Every quotation must belong to the bill supplier and project and cover the allocated budget item. Legacy `quotationId` remains valid for one quotation. Supplier bills that qualify for an existing exemption must state that intent with `--quotation-exempt`. Claims share budget allowance but never consume or link quotations, so quotation source fields are rejected for claims.
|
|
236
|
+
|
|
237
|
+
Every selected bill or claim line must belong to an active budget in `ESTIMATE_ACCEPTED` or `ESTIMATE_CLOSED`, including lines drawn from additional budgets in the project. The server checks this during selection, creation, editing, and approval. An unaccepted budget never qualifies a line for a quotation exemption. Existing pending bills expose budget and source-link blockers in `commitmentReview.blockers`.
|
|
238
|
+
|
|
239
|
+
Admins can repair missing source links on eligible draft, pending-approval, or rejected supplier bills with `bb bill quotation link <billId> --quotationIds q1,q2 --allow-state-change --allow-financial-write`. For same-line splits, use `--payload '{"quotationAllocations":[{"budgetItemId":"line_1","quotationId":"quote_1","amount":400},{"budgetItemId":"line_1","quotationId":"quote_2","amount":350}]}'`. The repair uses stored bill-line amounts and changes source linkage only.
|
|
240
|
+
|
|
241
|
+
```sh
|
|
242
|
+
bb bill create --payload '{"projectId":"project_1","budgetId":"budget_1","supplierId":"supplier_1","budgetItemIds":["line_1","line_2"],"quotationIds":["quote_1","quote_2"],"amount":1200,"isClaimable":false}'
|
|
243
|
+
|
|
244
|
+
bb bill create --payload '{"projectId":"project_1","budgetId":"budget_1","supplierId":"supplier_1","budgetItemIds":["line_1"],"amount":750,"lineAmounts":[{"budgetItemId":"line_1","amount":750}],"quotationAllocations":[{"budgetItemId":"line_1","quotationId":"quote_1","amount":400},{"budgetItemId":"line_1","quotationId":"quote_2","amount":350}],"isClaimable":false}'
|
|
245
|
+
```
|
|
246
|
+
|
|
235
247
|
Bill creation determines its required permissions from the parsed input, for both canonical and legacy commands. Ordinary unpaid creation requires state-change, financial-write, and external-write permissions, but no email permission. Already-paid creation also requires email permission. Without draft-only mode, existing role behavior remains: ordinary users create drafts, Accounting creates checked bills, and Admin creates approved bills and writes to QuickBooks. Ordinary creation can also adjust existing QuickBooks placeholder bills. Explicit drafts defer those adjustments until the later approval/QuickBooks workflow. When later authorized to submit the draft, use `bb bill approval create <billId>` with its state-change and email permissions; Admin-created drafts also enter finance review through this command. The static command catalog lists the union of possible effects.
|
|
236
248
|
|
|
237
249
|
**Approval email exclusions:** Approval requests create pending database records for every eligible approver, including configured non-recipient admin accounts. Automated approval-request emails skip those accounts.
|
|
@@ -244,6 +256,38 @@ Bill creation determines its required permissions from the parsed input, for bot
|
|
|
244
256
|
|
|
245
257
|
**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.
|
|
246
258
|
|
|
259
|
+
### Link an existing QuickBooks invoice
|
|
260
|
+
|
|
261
|
+
**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).
|
|
262
|
+
|
|
263
|
+
Authenticate and run `bb auth whoami` first. Preview and import use the same project management permission as the web flow: with user access checks enabled, Admins or the project's creator, business-development owner, inside-sales owner, or project manager can act. A finance/Lead role alone does not grant access to an unrelated project. The server applies its current access-check configuration and rechecks permission during confirmation.
|
|
264
|
+
|
|
265
|
+
Use the **BB budget ID** whose existing estimate should receive the invoice, and the numeric **QBO invoice transaction ID**, not the displayed invoice number or an estimate number. Accepted and closed estimates are supported; linking does not reopen them. Fully invoiced means all estimate coverage has been billed; it does not mean the invoices are fully paid.
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
bb customer-invoice preview-qbo <budgetId> <qboInvoiceId>
|
|
269
|
+
bb customer-invoice import-qbo <budgetId> <qboInvoiceId> --token <previewToken>
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Preview performs read-only QBO access and returns JSON under `data`, including `token`, project/estimate, invoice number/identity, currency, dates, total, outstanding `balance`, allocation `percentage`, lines, `alreadyLinked`, and other unlinked invoices. Review all of it before confirming. Import requires the exact `data.token` from that preview; it does not silently generate a fresh token. In an interactive terminal, confirm the named invoice and budget when prompted. For an explicitly authorized non-interactive import:
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
bb customer-invoice import-qbo <budgetId> <qboInvoiceId> \
|
|
276
|
+
--token <previewToken> --allow-state-change --allow-financial-write
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Import writes BB financial records only and needs neither `--allow-external-write` nor `--allow-email`. It calls the same server validation as the UI: project/customer/currency/estimate/allocation checks, sibling invoice coverage, and financial drift checks. It preserves the original invoice evidence and does not create, update, void, delete, send, or match anything in QBO. A successful response returns `batchId`, `invoiceId` (the new BB invoice ID), and `alreadyLinked: false`. Repeating the same link returns the original IDs with `alreadyLinked: true`, provided upstream financial evidence still agrees. A conflicting budget, ambiguous allocation, or financial mismatch fails; a stale preview requires another reviewed preview. Do not retry a conflict with a different budget to bypass it.
|
|
280
|
+
|
|
281
|
+
To refresh payments, use the returned **BB invoice ID**:
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
bb customer-invoice sync <invoiceId>
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
For `QBO_IMPORT` invoices, this existing command reads QBO and updates BB balance, payment status, and sync evidence only. Settlement becomes `PAID`; a reversal restores `APPROVED`; original invoice evidence is retained. Financial drift is reported separately as `qboFinancialDrift` and blocks further billing. The shared sync command also supports native invoices, so its generic automation permissions remain `--allow-state-change --allow-financial-write --allow-external-write`; imported records still take the server's BB-only path. Native creation continues to use `bb customer-invoice create --payload '<json>'` and needs no import token or fields.
|
|
288
|
+
|
|
289
|
+
The Seatrium example must be previewed against live QBO again before an operator imports it. Previously observed invoice numbers and paid/open balances are not authorization or current-state evidence.
|
|
290
|
+
|
|
247
291
|
**Customer-invoice workflow parity:** `bb customer-invoice list` uses the same global/project list procedure and metrics as the web pages; omit filters for the global list or use `--projectId` for project scope. `bb customer-invoice send --payload '<json>'` uses the same protected email workflow as the web composer, sends to the payload's `to` address, saves that address as the project's configured billing email after a successful send, and marks the invoice `SENT`. `bb customer-invoice approve` and `bb customer-invoice reject` are Admin-only batch operations: callers select a batch ID, and the API resolves its internal pending Admin approval record. `bb customer-invoice sync` follows QuickBooks' paid state and zero balance, restoring `SENT` or `APPROVED` if that payment is reversed. Invoice approval refuses voided invoices and closes the estimate only when approved invoice coverage totals 100%; deletion, voiding, rejection, expiry, and QBO synchronization use the same estimate-reopening and live-payment guards as the web app.
|
|
248
292
|
|
|
249
293
|
For agent-driven invoice work, use this read-before-write sequence:
|
|
@@ -283,9 +327,9 @@ Bill, claim, and quotation mutations accept only suppliers whose approval status
|
|
|
283
327
|
| Area | Legacy handler labels (reference only; non-exhaustive) |
|
|
284
328
|
| --- | --- |
|
|
285
329
|
| **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`) |
|
|
286
|
-
| **Bills / claims** | `list-bills` (`--isClaimable false` for bills, `--isClaimable true` for claims, omit for both), `list-claims` (claims only), `validate-bill-selection` (`--payload
|
|
330
|
+
| **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 `extractedAmount` and `amount`, with `amount <= extractedAmount`; 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` |
|
|
287
331
|
| **Quotations** | `list-quotations` (supports project, budget, supplier, status, requester, and text-search filters), `get-quotation`, `upload-quotation-attachment` (`<projectId>` + PDF/JPEG/PNG path up to 20MB; returns attachment JSON for payload use), `cleanup-staged-quotation-attachments`, `create-quotation` (`--payload` for `quotation.createDraft`; include `amountWithoutGst` and `gstAmount`), `update-quotation` (`--payload`; updates a `DRAFT` or `REJECTED` quotation), `delete-quotation`, `submit-quotation`, `approve-quotation`, `reject-quotation`, `download-quotation-pdf` (`original`, `staff`, or `final`) |
|
|
288
|
-
| **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`, `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` |
|
|
332
|
+
| **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` |
|
|
289
333
|
| **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` |
|
|
290
334
|
| **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`) |
|
|
291
335
|
| **Contacts** | `list-contacts`, `create-contact-person` (`--payload`), `update-contact-person` (`--payload`) |
|
|
@@ -359,5 +403,7 @@ relationships or allocations block approval and require review. Multi-line quota
|
|
|
359
403
|
create/update payloads must include `lineAmounts` with `{ budgetItemId, amount }`
|
|
360
404
|
for every selected line, summing to `amountWithoutGst`. The server revalidates
|
|
361
405
|
existing quotation links, reviewed usage, and per-line allowance inside the
|
|
362
|
-
approval transaction.
|
|
363
|
-
|
|
406
|
+
approval transaction. Multiple source quotations use `BillLineItem.quotationId`;
|
|
407
|
+
apply the updated database schema and deploy the API before releasing the CLI.
|
|
408
|
+
Legacy bill-wide links remain readable, and unresolved historical overlaps
|
|
409
|
+
continue to block approval.
|
package/command-manifest.json
CHANGED
|
@@ -2108,6 +2108,10 @@
|
|
|
2108
2108
|
{
|
|
2109
2109
|
"name": "--draft-only",
|
|
2110
2110
|
"description": "Create a draft for every role; no approval, email, or QuickBooks writes. Conflicts with alreadyPaid: true."
|
|
2111
|
+
},
|
|
2112
|
+
{
|
|
2113
|
+
"name": "--quotation-exempt",
|
|
2114
|
+
"description": "Explicitly request an eligible server-validated supplier-bill quotation exemption. Cannot be combined with quotation IDs."
|
|
2111
2115
|
}
|
|
2112
2116
|
],
|
|
2113
2117
|
"argumentMode": "legacy-passthrough",
|
|
@@ -2158,12 +2162,78 @@
|
|
|
2158
2162
|
{
|
|
2159
2163
|
"name": "--allow-financial-write",
|
|
2160
2164
|
"description": "Allow financial-record changes in non-interactive use."
|
|
2165
|
+
},
|
|
2166
|
+
{
|
|
2167
|
+
"name": "--quotationIds <csv>",
|
|
2168
|
+
"description": "Approved quotation IDs for a supplier bill. Each selected budget item must be covered once."
|
|
2169
|
+
},
|
|
2170
|
+
{
|
|
2171
|
+
"name": "--quotationId <id>",
|
|
2172
|
+
"description": "Legacy syntax for selecting one approved quotation."
|
|
2161
2173
|
}
|
|
2162
2174
|
],
|
|
2163
2175
|
"argumentMode": "legacy-passthrough",
|
|
2164
2176
|
"effects": [],
|
|
2165
2177
|
"legacyTarget": "validate-bill-selection"
|
|
2166
2178
|
},
|
|
2179
|
+
{
|
|
2180
|
+
"path": ["bill", "quotation", "link"],
|
|
2181
|
+
"legacyAliases": ["link-bill-quotations", "link_bill_quotations", "billId"],
|
|
2182
|
+
"summary": "Link Bill Quotations.",
|
|
2183
|
+
"globalOptions": [
|
|
2184
|
+
{
|
|
2185
|
+
"name": "--help",
|
|
2186
|
+
"description": "Show help for this command."
|
|
2187
|
+
},
|
|
2188
|
+
{
|
|
2189
|
+
"name": "--quiet",
|
|
2190
|
+
"description": "Suppress non-error diagnostics."
|
|
2191
|
+
},
|
|
2192
|
+
{
|
|
2193
|
+
"name": "--debug",
|
|
2194
|
+
"description": "Emit sanitized diagnostic traces."
|
|
2195
|
+
},
|
|
2196
|
+
{
|
|
2197
|
+
"name": "--api-url",
|
|
2198
|
+
"description": "Override the Budget Builder API base URL."
|
|
2199
|
+
},
|
|
2200
|
+
{
|
|
2201
|
+
"name": "--no-browser",
|
|
2202
|
+
"description": "Print the authorization URL without opening a browser."
|
|
2203
|
+
},
|
|
2204
|
+
{
|
|
2205
|
+
"name": "--allow-state-change",
|
|
2206
|
+
"description": "Allow a Budget Builder state change in non-interactive use."
|
|
2207
|
+
},
|
|
2208
|
+
{
|
|
2209
|
+
"name": "--allow-email",
|
|
2210
|
+
"description": "Allow sending email in non-interactive use."
|
|
2211
|
+
},
|
|
2212
|
+
{
|
|
2213
|
+
"name": "--allow-external-write",
|
|
2214
|
+
"description": "Allow writes to external systems in non-interactive use."
|
|
2215
|
+
},
|
|
2216
|
+
{
|
|
2217
|
+
"name": "--allow-delete",
|
|
2218
|
+
"description": "Allow deleting data in non-interactive use."
|
|
2219
|
+
},
|
|
2220
|
+
{
|
|
2221
|
+
"name": "--allow-financial-write",
|
|
2222
|
+
"description": "Allow financial-record changes in non-interactive use."
|
|
2223
|
+
},
|
|
2224
|
+
{
|
|
2225
|
+
"name": "--payload <json>",
|
|
2226
|
+
"description": "Explicit quotationAllocations for split source coverage on the bill's stored line amounts."
|
|
2227
|
+
},
|
|
2228
|
+
{
|
|
2229
|
+
"name": "--quotationIds <csv>",
|
|
2230
|
+
"description": "Approved quotation IDs to link across the bill's disjoint line coverage."
|
|
2231
|
+
}
|
|
2232
|
+
],
|
|
2233
|
+
"argumentMode": "legacy-passthrough",
|
|
2234
|
+
"effects": ["state-change", "financial-write"],
|
|
2235
|
+
"legacyTarget": "link-bill-quotations"
|
|
2236
|
+
},
|
|
2167
2237
|
{
|
|
2168
2238
|
"path": ["bill", "update"],
|
|
2169
2239
|
"legacyAliases": ["update-bill", "update_bill"],
|
|
@@ -3643,6 +3713,112 @@
|
|
|
3643
3713
|
"effects": [],
|
|
3644
3714
|
"legacyTarget": "get-customer-invoice-email-context"
|
|
3645
3715
|
},
|
|
3716
|
+
{
|
|
3717
|
+
"path": ["customer-invoice", "preview-qbo"],
|
|
3718
|
+
"legacyAliases": [
|
|
3719
|
+
"preview-qbo-customer-invoice",
|
|
3720
|
+
"preview_qbo_customer_invoice"
|
|
3721
|
+
],
|
|
3722
|
+
"summary": "Preview an existing QuickBooks invoice for a BB budget using read-only QBO access. Arguments: <budgetId> <qboInvoiceId>.",
|
|
3723
|
+
"globalOptions": [
|
|
3724
|
+
{
|
|
3725
|
+
"name": "--help",
|
|
3726
|
+
"description": "Show help for this command."
|
|
3727
|
+
},
|
|
3728
|
+
{
|
|
3729
|
+
"name": "--quiet",
|
|
3730
|
+
"description": "Suppress non-error diagnostics."
|
|
3731
|
+
},
|
|
3732
|
+
{
|
|
3733
|
+
"name": "--debug",
|
|
3734
|
+
"description": "Emit sanitized diagnostic traces."
|
|
3735
|
+
},
|
|
3736
|
+
{
|
|
3737
|
+
"name": "--api-url",
|
|
3738
|
+
"description": "Override the Budget Builder API base URL."
|
|
3739
|
+
},
|
|
3740
|
+
{
|
|
3741
|
+
"name": "--no-browser",
|
|
3742
|
+
"description": "Print the authorization URL without opening a browser."
|
|
3743
|
+
},
|
|
3744
|
+
{
|
|
3745
|
+
"name": "--allow-state-change",
|
|
3746
|
+
"description": "Allow a Budget Builder state change in non-interactive use."
|
|
3747
|
+
},
|
|
3748
|
+
{
|
|
3749
|
+
"name": "--allow-email",
|
|
3750
|
+
"description": "Allow sending email in non-interactive use."
|
|
3751
|
+
},
|
|
3752
|
+
{
|
|
3753
|
+
"name": "--allow-external-write",
|
|
3754
|
+
"description": "Allow writes to external systems in non-interactive use."
|
|
3755
|
+
},
|
|
3756
|
+
{
|
|
3757
|
+
"name": "--allow-delete",
|
|
3758
|
+
"description": "Allow deleting data in non-interactive use."
|
|
3759
|
+
},
|
|
3760
|
+
{
|
|
3761
|
+
"name": "--allow-financial-write",
|
|
3762
|
+
"description": "Allow financial-record changes in non-interactive use."
|
|
3763
|
+
}
|
|
3764
|
+
],
|
|
3765
|
+
"argumentMode": "legacy-passthrough",
|
|
3766
|
+
"effects": [],
|
|
3767
|
+
"legacyTarget": "preview-qbo-customer-invoice"
|
|
3768
|
+
},
|
|
3769
|
+
{
|
|
3770
|
+
"path": ["customer-invoice", "import-qbo"],
|
|
3771
|
+
"legacyAliases": [
|
|
3772
|
+
"import-qbo-customer-invoice",
|
|
3773
|
+
"import_qbo_customer_invoice"
|
|
3774
|
+
],
|
|
3775
|
+
"summary": "Link an existing QuickBooks invoice in BB only after reviewing preview-qbo. Arguments: <budgetId> <qboInvoiceId> --token <previewToken>. Supports closed estimates; no QBO writes or emails.",
|
|
3776
|
+
"globalOptions": [
|
|
3777
|
+
{
|
|
3778
|
+
"name": "--help",
|
|
3779
|
+
"description": "Show help for this command."
|
|
3780
|
+
},
|
|
3781
|
+
{
|
|
3782
|
+
"name": "--quiet",
|
|
3783
|
+
"description": "Suppress non-error diagnostics."
|
|
3784
|
+
},
|
|
3785
|
+
{
|
|
3786
|
+
"name": "--debug",
|
|
3787
|
+
"description": "Emit sanitized diagnostic traces."
|
|
3788
|
+
},
|
|
3789
|
+
{
|
|
3790
|
+
"name": "--api-url",
|
|
3791
|
+
"description": "Override the Budget Builder API base URL."
|
|
3792
|
+
},
|
|
3793
|
+
{
|
|
3794
|
+
"name": "--no-browser",
|
|
3795
|
+
"description": "Print the authorization URL without opening a browser."
|
|
3796
|
+
},
|
|
3797
|
+
{
|
|
3798
|
+
"name": "--allow-state-change",
|
|
3799
|
+
"description": "Allow a Budget Builder state change in non-interactive use."
|
|
3800
|
+
},
|
|
3801
|
+
{
|
|
3802
|
+
"name": "--allow-email",
|
|
3803
|
+
"description": "Allow sending email in non-interactive use."
|
|
3804
|
+
},
|
|
3805
|
+
{
|
|
3806
|
+
"name": "--allow-external-write",
|
|
3807
|
+
"description": "Allow writes to external systems in non-interactive use."
|
|
3808
|
+
},
|
|
3809
|
+
{
|
|
3810
|
+
"name": "--allow-delete",
|
|
3811
|
+
"description": "Allow deleting data in non-interactive use."
|
|
3812
|
+
},
|
|
3813
|
+
{
|
|
3814
|
+
"name": "--allow-financial-write",
|
|
3815
|
+
"description": "Allow financial-record changes in non-interactive use."
|
|
3816
|
+
}
|
|
3817
|
+
],
|
|
3818
|
+
"argumentMode": "legacy-passthrough",
|
|
3819
|
+
"effects": ["state-change", "financial-write"],
|
|
3820
|
+
"legacyTarget": "import-qbo-customer-invoice"
|
|
3821
|
+
},
|
|
3646
3822
|
{
|
|
3647
3823
|
"path": ["customer-invoice", "create"],
|
|
3648
3824
|
"legacyAliases": ["create-customer-invoice", "create_customer_invoice"],
|
package/command-reference.md
CHANGED
|
@@ -361,6 +361,14 @@ Legacy aliases: `validate-bill-selection`, `validate_bill_selection`.
|
|
|
361
361
|
|
|
362
362
|
Effects: none.
|
|
363
363
|
|
|
364
|
+
## `bb bill quotation link`
|
|
365
|
+
|
|
366
|
+
Link Bill Quotations.
|
|
367
|
+
|
|
368
|
+
Legacy aliases: `link-bill-quotations`, `link_bill_quotations`, `billId`.
|
|
369
|
+
|
|
370
|
+
Effects: state-change, financial-write.
|
|
371
|
+
|
|
364
372
|
## `bb bill update`
|
|
365
373
|
|
|
366
374
|
Update Bill.
|
|
@@ -593,6 +601,22 @@ Legacy aliases: `get-customer-invoice-email-context`, `get_customer_invoice_emai
|
|
|
593
601
|
|
|
594
602
|
Effects: none.
|
|
595
603
|
|
|
604
|
+
## `bb customer-invoice preview-qbo`
|
|
605
|
+
|
|
606
|
+
Preview an existing QuickBooks invoice for a BB budget using read-only QBO access. Arguments: <budgetId> <qboInvoiceId>.
|
|
607
|
+
|
|
608
|
+
Legacy aliases: `preview-qbo-customer-invoice`, `preview_qbo_customer_invoice`.
|
|
609
|
+
|
|
610
|
+
Effects: none.
|
|
611
|
+
|
|
612
|
+
## `bb customer-invoice import-qbo`
|
|
613
|
+
|
|
614
|
+
Link an existing QuickBooks invoice in BB only after reviewing preview-qbo. Arguments: <budgetId> <qboInvoiceId> --token <previewToken>. Supports closed estimates; no QBO writes or emails.
|
|
615
|
+
|
|
616
|
+
Legacy aliases: `import-qbo-customer-invoice`, `import_qbo_customer_invoice`.
|
|
617
|
+
|
|
618
|
+
Effects: state-change, financial-write.
|
|
619
|
+
|
|
596
620
|
## `bb customer-invoice create`
|
|
597
621
|
|
|
598
622
|
Create Customer Invoice.
|
package/dist/index.js
CHANGED
|
@@ -20154,6 +20154,120 @@ function optionalString(v) {
|
|
|
20154
20154
|
const s = String(v);
|
|
20155
20155
|
return s === "" ? undefined : s;
|
|
20156
20156
|
}
|
|
20157
|
+
var parseQuotationReferences = (input2, commandName) => {
|
|
20158
|
+
const quotationId = optionalString(input2.quotationId)?.trim();
|
|
20159
|
+
const rawAllocations = input2.quotationAllocations;
|
|
20160
|
+
let quotationAllocations;
|
|
20161
|
+
if (rawAllocations !== undefined && rawAllocations !== null) {
|
|
20162
|
+
if (!Array.isArray(rawAllocations) || rawAllocations.length === 0) {
|
|
20163
|
+
throw new Error(`${commandName} payload.quotationAllocations must be a non-empty array.`);
|
|
20164
|
+
}
|
|
20165
|
+
quotationAllocations = rawAllocations.map((raw, index) => {
|
|
20166
|
+
const label = `${commandName} payload.quotationAllocations[${index}]`;
|
|
20167
|
+
const allocation = requireObject(raw, label);
|
|
20168
|
+
const amount = Number(allocation.amount);
|
|
20169
|
+
if (!Number.isFinite(amount) || amount <= 0 || !Number.isInteger(amount * 100)) {
|
|
20170
|
+
throw new Error(`${label}.amount must be a positive cent value.`);
|
|
20171
|
+
}
|
|
20172
|
+
return {
|
|
20173
|
+
budgetItemId: requiredString(allocation.budgetItemId, `${label}.budgetItemId`),
|
|
20174
|
+
quotationId: requiredString(allocation.quotationId, `${label}.quotationId`),
|
|
20175
|
+
amount
|
|
20176
|
+
};
|
|
20177
|
+
});
|
|
20178
|
+
const keys = quotationAllocations.map((allocation) => `${allocation.budgetItemId}\x00${allocation.quotationId}`);
|
|
20179
|
+
if (new Set(keys).size !== keys.length) {
|
|
20180
|
+
throw new Error(`${commandName} payload.quotationAllocations must not repeat a budget-item/quotation pair.`);
|
|
20181
|
+
}
|
|
20182
|
+
}
|
|
20183
|
+
const rawQuotationIds = input2.quotationIds;
|
|
20184
|
+
let quotationIds;
|
|
20185
|
+
if (rawQuotationIds !== undefined && rawQuotationIds !== null) {
|
|
20186
|
+
if (!Array.isArray(rawQuotationIds)) {
|
|
20187
|
+
throw new Error(`${commandName} payload.quotationIds must be a string array.`);
|
|
20188
|
+
}
|
|
20189
|
+
quotationIds = rawQuotationIds.map((id, index) => requiredString(id, `${commandName} payload.quotationIds[${index}]`));
|
|
20190
|
+
if (new Set(quotationIds).size !== quotationIds.length) {
|
|
20191
|
+
throw new Error(`${commandName} payload.quotationIds must not contain duplicates.`);
|
|
20192
|
+
}
|
|
20193
|
+
}
|
|
20194
|
+
if (quotationId && quotationIds && (quotationIds.length !== 1 || quotationIds[0] !== quotationId)) {
|
|
20195
|
+
throw new Error(`${commandName} payload.quotationId conflicts with payload.quotationIds.`);
|
|
20196
|
+
}
|
|
20197
|
+
if (quotationAllocations && quotationIds) {
|
|
20198
|
+
const allocationIds = [
|
|
20199
|
+
...new Set(quotationAllocations.map((allocation) => allocation.quotationId))
|
|
20200
|
+
].sort();
|
|
20201
|
+
const selectedIds = [...quotationIds].sort();
|
|
20202
|
+
if (allocationIds.join("\x00") !== selectedIds.join("\x00")) {
|
|
20203
|
+
throw new Error(`${commandName} payload.quotationIds conflicts with payload.quotationAllocations.`);
|
|
20204
|
+
}
|
|
20205
|
+
}
|
|
20206
|
+
return {
|
|
20207
|
+
...quotationId ? { quotationId } : {},
|
|
20208
|
+
...quotationIds ? { quotationIds } : {},
|
|
20209
|
+
...quotationAllocations ? { quotationAllocations } : {}
|
|
20210
|
+
};
|
|
20211
|
+
};
|
|
20212
|
+
var parseBillLineAmounts = (input2, commandName) => {
|
|
20213
|
+
if (input2.lineAmounts === undefined || input2.lineAmounts === null)
|
|
20214
|
+
return;
|
|
20215
|
+
if (!Array.isArray(input2.lineAmounts) || input2.lineAmounts.length === 0) {
|
|
20216
|
+
throw new Error(`${commandName} payload.lineAmounts must be a non-empty array.`);
|
|
20217
|
+
}
|
|
20218
|
+
const lineAmounts = input2.lineAmounts.map((raw, index) => {
|
|
20219
|
+
const label = `${commandName} payload.lineAmounts[${index}]`;
|
|
20220
|
+
const line = requireObject(raw, label);
|
|
20221
|
+
const amount = Number(line.amount);
|
|
20222
|
+
if (!Number.isFinite(amount) || amount < 0 || !Number.isInteger(amount * 100)) {
|
|
20223
|
+
throw new Error(`${label}.amount must be a non-negative cent value.`);
|
|
20224
|
+
}
|
|
20225
|
+
return {
|
|
20226
|
+
budgetItemId: requiredString(line.budgetItemId, `${label}.budgetItemId`),
|
|
20227
|
+
amount
|
|
20228
|
+
};
|
|
20229
|
+
});
|
|
20230
|
+
if (new Set(lineAmounts.map((line) => line.budgetItemId)).size !== lineAmounts.length) {
|
|
20231
|
+
throw new Error(`${commandName} payload.lineAmounts must contain each budget item once.`);
|
|
20232
|
+
}
|
|
20233
|
+
return lineAmounts;
|
|
20234
|
+
};
|
|
20235
|
+
var cents = (amount) => Math.round(amount * 100);
|
|
20236
|
+
var validateExplicitBillAllocations = ({
|
|
20237
|
+
amount,
|
|
20238
|
+
budgetItemIds,
|
|
20239
|
+
commandName,
|
|
20240
|
+
lineAmounts,
|
|
20241
|
+
quotationAllocations
|
|
20242
|
+
}) => {
|
|
20243
|
+
if (!lineAmounts && !quotationAllocations)
|
|
20244
|
+
return;
|
|
20245
|
+
if (!lineAmounts) {
|
|
20246
|
+
throw new Error(`${commandName} payload.lineAmounts is required with quotationAllocations.`);
|
|
20247
|
+
}
|
|
20248
|
+
const selected = new Set(budgetItemIds);
|
|
20249
|
+
if (lineAmounts.length !== selected.size || lineAmounts.some((line) => !selected.has(line.budgetItemId))) {
|
|
20250
|
+
throw new Error(`${commandName} payload.lineAmounts must contain each selected budget item exactly once.`);
|
|
20251
|
+
}
|
|
20252
|
+
if (lineAmounts.reduce((sum, line) => sum + cents(line.amount), 0) !== cents(amount)) {
|
|
20253
|
+
throw new Error(`${commandName} payload.lineAmounts must sum to payload.amount.`);
|
|
20254
|
+
}
|
|
20255
|
+
if (!quotationAllocations)
|
|
20256
|
+
return;
|
|
20257
|
+
const lineAmountById = new Map(lineAmounts.map((line) => [line.budgetItemId, cents(line.amount)]));
|
|
20258
|
+
const allocatedById = new Map;
|
|
20259
|
+
for (const allocation of quotationAllocations) {
|
|
20260
|
+
if (!selected.has(allocation.budgetItemId)) {
|
|
20261
|
+
throw new Error(`${commandName} payload.quotationAllocations references an unselected budget item.`);
|
|
20262
|
+
}
|
|
20263
|
+
allocatedById.set(allocation.budgetItemId, (allocatedById.get(allocation.budgetItemId) ?? 0) + cents(allocation.amount));
|
|
20264
|
+
}
|
|
20265
|
+
for (const [budgetItemId, allocated] of allocatedById) {
|
|
20266
|
+
if (allocated > (lineAmountById.get(budgetItemId) ?? 0)) {
|
|
20267
|
+
throw new Error(`${commandName} quotation allocations exceed the line amount for ${budgetItemId}.`);
|
|
20268
|
+
}
|
|
20269
|
+
}
|
|
20270
|
+
};
|
|
20157
20271
|
function requiredString(value, fieldLabel) {
|
|
20158
20272
|
if (value === undefined || value === null) {
|
|
20159
20273
|
throw new Error(`${fieldLabel} is required.`);
|
|
@@ -20259,7 +20373,7 @@ var billAttachment = (x, i, field = "attachments") => {
|
|
|
20259
20373
|
size: Number(a.size)
|
|
20260
20374
|
};
|
|
20261
20375
|
};
|
|
20262
|
-
function parseCreateBillPayload(raw, draftOnlyFlag) {
|
|
20376
|
+
function parseCreateBillPayload(raw, draftOnlyFlag, quotationExempt = false, requireSupplierQuotation = true) {
|
|
20263
20377
|
const o = requireObject(raw, "create-bill payload");
|
|
20264
20378
|
const isClaimable = o.isClaimable;
|
|
20265
20379
|
if (typeof isClaimable !== "boolean") {
|
|
@@ -20315,13 +20429,36 @@ function parseCreateBillPayload(raw, draftOnlyFlag) {
|
|
|
20315
20429
|
if (!Number.isFinite(amount) || amount < 0) {
|
|
20316
20430
|
throw new Error("create-bill payload.amount must be a number >= 0.");
|
|
20317
20431
|
}
|
|
20432
|
+
const quotationReferences = parseQuotationReferences(o, "create-bill");
|
|
20433
|
+
const parsedBudgetItemIds = budgetItemIds.map((id, i) => requiredString(id, `create-bill payload.budgetItemIds[${i}]`));
|
|
20434
|
+
const lineAmounts = parseBillLineAmounts(o, "create-bill");
|
|
20435
|
+
validateExplicitBillAllocations({
|
|
20436
|
+
amount,
|
|
20437
|
+
budgetItemIds: parsedBudgetItemIds,
|
|
20438
|
+
commandName: "create-bill",
|
|
20439
|
+
lineAmounts,
|
|
20440
|
+
quotationAllocations: quotationReferences.quotationAllocations
|
|
20441
|
+
});
|
|
20442
|
+
if (quotationExempt && isClaimable) {
|
|
20443
|
+
throw new Error("--quotation-exempt is only valid for supplier bills.");
|
|
20444
|
+
}
|
|
20445
|
+
if (quotationExempt && (quotationReferences.quotationId || (quotationReferences.quotationIds?.length ?? 0) > 0 || (quotationReferences.quotationAllocations?.length ?? 0) > 0)) {
|
|
20446
|
+
throw new Error("--quotation-exempt cannot be combined with payload quotation IDs.");
|
|
20447
|
+
}
|
|
20448
|
+
if (isClaimable && (quotationReferences.quotationId || (quotationReferences.quotationIds?.length ?? 0) > 0 || (quotationReferences.quotationAllocations?.length ?? 0) > 0)) {
|
|
20449
|
+
throw new Error("Employee claims cannot include quotation source inputs.");
|
|
20450
|
+
}
|
|
20451
|
+
if (requireSupplierQuotation && !quotationExempt && !isClaimable && !quotationReferences.quotationId && (quotationReferences.quotationIds?.length ?? 0) === 0 && (quotationReferences.quotationAllocations?.length ?? 0) === 0) {
|
|
20452
|
+
throw new Error("create-bill supplier bills require payload.quotationAllocations, payload.quotationIds, or legacy payload.quotationId.");
|
|
20453
|
+
}
|
|
20318
20454
|
return {
|
|
20319
20455
|
budgetId: requiredString(o.budgetId, "create-bill payload.budgetId"),
|
|
20320
20456
|
projectId: requiredString(o.projectId, "create-bill payload.projectId"),
|
|
20321
20457
|
supplierId: requiredString(o.supplierId, "create-bill payload.supplierId"),
|
|
20322
|
-
|
|
20458
|
+
...quotationReferences,
|
|
20323
20459
|
amount,
|
|
20324
|
-
budgetItemIds:
|
|
20460
|
+
budgetItemIds: parsedBudgetItemIds,
|
|
20461
|
+
...lineAmounts ? { lineAmounts } : {},
|
|
20325
20462
|
comment: optionalString(o.comment),
|
|
20326
20463
|
isClaimable,
|
|
20327
20464
|
attachments,
|
|
@@ -20339,7 +20476,7 @@ function parseCreateBillPayload(raw, draftOnlyFlag) {
|
|
|
20339
20476
|
}
|
|
20340
20477
|
function parseUpdateBillPayload(raw) {
|
|
20341
20478
|
const o = requireObject(raw, "update-bill payload");
|
|
20342
|
-
const { draftOnly, ...input2 } = parseCreateBillPayload(raw);
|
|
20479
|
+
const { draftOnly, ...input2 } = parseCreateBillPayload(raw, undefined, false, false);
|
|
20343
20480
|
if (draftOnly !== undefined) {
|
|
20344
20481
|
throw new Error("draftOnly is only supported when creating a bill.");
|
|
20345
20482
|
}
|
|
@@ -20348,9 +20485,60 @@ function parseUpdateBillPayload(raw) {
|
|
|
20348
20485
|
id: requiredString(o.id, "update-bill payload.id")
|
|
20349
20486
|
};
|
|
20350
20487
|
}
|
|
20351
|
-
function parseValidateBillSelectionPayload(raw) {
|
|
20352
|
-
requireObject(raw, "validate-bill-selection payload");
|
|
20353
|
-
|
|
20488
|
+
function parseValidateBillSelectionPayload(raw, quotationFlags = {}) {
|
|
20489
|
+
const input2 = requireObject(raw, "validate-bill-selection payload");
|
|
20490
|
+
const payloadReferences = parseQuotationReferences(input2, "validate-bill-selection");
|
|
20491
|
+
const flagReferences = parseQuotationReferences({ ...quotationFlags }, "validate-bill-selection flags");
|
|
20492
|
+
const payloadIds = payloadReferences.quotationIds ?? (payloadReferences.quotationId ? [payloadReferences.quotationId] : undefined);
|
|
20493
|
+
const flagIds = flagReferences.quotationIds ?? (flagReferences.quotationId ? [flagReferences.quotationId] : undefined);
|
|
20494
|
+
if (payloadIds && flagIds && payloadIds.join("\x00") !== flagIds.join("\x00")) {
|
|
20495
|
+
throw new Error("validate-bill-selection quotation flags conflict with the payload quotation IDs.");
|
|
20496
|
+
}
|
|
20497
|
+
const isClaimable = input2.isClaimable;
|
|
20498
|
+
if (isClaimable !== undefined && typeof isClaimable !== "boolean") {
|
|
20499
|
+
throw new Error("validate-bill-selection payload.isClaimable must be boolean.");
|
|
20500
|
+
}
|
|
20501
|
+
if (isClaimable === true && (payloadReferences.quotationId || (payloadReferences.quotationIds?.length ?? 0) > 0 || (payloadReferences.quotationAllocations?.length ?? 0) > 0 || !!flagReferences.quotationId || (flagReferences.quotationIds?.length ?? 0) > 0)) {
|
|
20502
|
+
throw new Error("Employee claims cannot include quotation source inputs.");
|
|
20503
|
+
}
|
|
20504
|
+
const lineAmounts = parseBillLineAmounts(input2, "validate-bill-selection");
|
|
20505
|
+
const budgetItemIds = Array.isArray(input2.budgetItemIds) ? input2.budgetItemIds.map((id, index) => requiredString(id, `validate-bill-selection payload.budgetItemIds[${index}]`)) : [];
|
|
20506
|
+
const amount = input2.amount === undefined || input2.amount === null ? undefined : Number(input2.amount);
|
|
20507
|
+
if (lineAmounts || payloadReferences.quotationAllocations) {
|
|
20508
|
+
if (amount === undefined || !Number.isFinite(amount) || budgetItemIds.length === 0) {
|
|
20509
|
+
throw new Error("validate-bill-selection explicit allocations require payload.amount and payload.budgetItemIds.");
|
|
20510
|
+
}
|
|
20511
|
+
validateExplicitBillAllocations({
|
|
20512
|
+
amount,
|
|
20513
|
+
budgetItemIds,
|
|
20514
|
+
commandName: "validate-bill-selection",
|
|
20515
|
+
lineAmounts,
|
|
20516
|
+
quotationAllocations: payloadReferences.quotationAllocations
|
|
20517
|
+
});
|
|
20518
|
+
}
|
|
20519
|
+
return {
|
|
20520
|
+
...input2,
|
|
20521
|
+
...amount !== undefined ? { amount } : {},
|
|
20522
|
+
...isClaimable !== undefined ? { isClaimable } : {},
|
|
20523
|
+
...lineAmounts ? { lineAmounts } : {},
|
|
20524
|
+
...payloadReferences,
|
|
20525
|
+
...flagReferences
|
|
20526
|
+
};
|
|
20527
|
+
}
|
|
20528
|
+
function parseLinkBillQuotationsPayload(billId, raw) {
|
|
20529
|
+
const input2 = requireObject(raw, "link-bill-quotations payload");
|
|
20530
|
+
const references = parseQuotationReferences(input2, "link-bill-quotations");
|
|
20531
|
+
if (references.quotationId) {
|
|
20532
|
+
throw new Error("link-bill-quotations payload uses quotationIds or quotationAllocations; legacy quotationId is not supported.");
|
|
20533
|
+
}
|
|
20534
|
+
if ((references.quotationIds?.length ?? 0) === 0 && (references.quotationAllocations?.length ?? 0) === 0) {
|
|
20535
|
+
throw new Error("link-bill-quotations payload requires quotationIds or quotationAllocations.");
|
|
20536
|
+
}
|
|
20537
|
+
return {
|
|
20538
|
+
billId: requiredString(billId, "billId"),
|
|
20539
|
+
...references.quotationIds ? { quotationIds: references.quotationIds } : {},
|
|
20540
|
+
...references.quotationAllocations ? { quotationAllocations: references.quotationAllocations } : {}
|
|
20541
|
+
};
|
|
20354
20542
|
}
|
|
20355
20543
|
function parseUpdateBillPaymentEvidencePayload(raw) {
|
|
20356
20544
|
requireObject(raw, "update-bill-payment-evidence payload");
|
|
@@ -22048,8 +22236,8 @@ async function deleteItemCategoriesByIds(ids) {
|
|
|
22048
22236
|
const result = await api2.itemCategory.deleteItemCategories.mutate({ ids });
|
|
22049
22237
|
out(result);
|
|
22050
22238
|
}
|
|
22051
|
-
async function createBillFromPayload(raw, draftOnlyFlag) {
|
|
22052
|
-
const input2 = parseCreateBillPayload(raw, draftOnlyFlag);
|
|
22239
|
+
async function createBillFromPayload(raw, draftOnlyFlag, quotationExempt = false) {
|
|
22240
|
+
const input2 = parseCreateBillPayload(raw, draftOnlyFlag, quotationExempt);
|
|
22053
22241
|
await confirmCurrentCommand({
|
|
22054
22242
|
action: input2.draftOnly ? "bill create (draft only)" : "bill create",
|
|
22055
22243
|
target: `project ${input2.projectId}, supplier ${input2.supplierId}`,
|
|
@@ -22134,6 +22322,19 @@ var getCustomerInvoiceEmailContext = async (batchId) => {
|
|
|
22134
22322
|
});
|
|
22135
22323
|
out(result);
|
|
22136
22324
|
};
|
|
22325
|
+
var previewQboCustomerInvoice = async (input2) => {
|
|
22326
|
+
const result = await api2.customerInvoice.previewExistingInvoice.mutate(input2);
|
|
22327
|
+
out(result);
|
|
22328
|
+
};
|
|
22329
|
+
var importQboCustomerInvoice = async (input2) => {
|
|
22330
|
+
await confirmCurrentCommand({
|
|
22331
|
+
action: "Link existing QuickBooks invoice",
|
|
22332
|
+
target: `QuickBooks invoice ${input2.qboInvoiceId} to budget ${input2.budgetId}`,
|
|
22333
|
+
details: "records the reviewed invoice in Budget Builder only; QuickBooks access is read-only; preserves original evidence and revalidates the preview token"
|
|
22334
|
+
});
|
|
22335
|
+
const result = await api2.customerInvoice.linkExistingInvoice.mutate(input2);
|
|
22336
|
+
out(result);
|
|
22337
|
+
};
|
|
22137
22338
|
var createCustomerInvoice = async (raw) => {
|
|
22138
22339
|
const input2 = parseCreateCustomerInvoicePayload(raw);
|
|
22139
22340
|
await assertSensitiveWorkflowConfirmed({
|
|
@@ -22237,11 +22438,16 @@ async function updateBillFromPayload(raw) {
|
|
|
22237
22438
|
const result = await api2.bill.update.mutate(input2);
|
|
22238
22439
|
out(result);
|
|
22239
22440
|
}
|
|
22240
|
-
var validateBillSelectionFromPayload = async (raw) => {
|
|
22241
|
-
const input2 = parseValidateBillSelectionPayload(raw);
|
|
22441
|
+
var validateBillSelectionFromPayload = async (raw, quotationFlags) => {
|
|
22442
|
+
const input2 = parseValidateBillSelectionPayload(raw, quotationFlags);
|
|
22242
22443
|
const result = await api2.bill.validateSelection.mutate(input2);
|
|
22243
22444
|
out(result);
|
|
22244
22445
|
};
|
|
22446
|
+
var linkBillQuotations = async (billId, raw) => {
|
|
22447
|
+
const input2 = parseLinkBillQuotationsPayload(billId, raw);
|
|
22448
|
+
const result = await api2.bill.linkQuotations.mutate(input2);
|
|
22449
|
+
out(result);
|
|
22450
|
+
};
|
|
22245
22451
|
var updateBillPaymentEvidenceFromPayload = async (raw) => {
|
|
22246
22452
|
const input2 = parseUpdateBillPaymentEvidencePayload(raw);
|
|
22247
22453
|
const result = await api2.bill.updatePaymentEvidence.mutate(input2);
|
|
@@ -23079,6 +23285,7 @@ var mutationPrefixes = [
|
|
|
23079
23285
|
"delete-",
|
|
23080
23286
|
"discard-",
|
|
23081
23287
|
"import-",
|
|
23288
|
+
"link-",
|
|
23082
23289
|
"mark-",
|
|
23083
23290
|
"patch-",
|
|
23084
23291
|
"reactivate-",
|
|
@@ -23166,6 +23373,8 @@ var externalWriteTargets = new Set([
|
|
|
23166
23373
|
"void-customer-invoice"
|
|
23167
23374
|
]);
|
|
23168
23375
|
var financialWriteTargets = new Set([
|
|
23376
|
+
"import-qbo-customer-invoice",
|
|
23377
|
+
"link-bill-quotations",
|
|
23169
23378
|
"approve-bill",
|
|
23170
23379
|
"approve-customer-invoice",
|
|
23171
23380
|
"complete-project",
|
|
@@ -23223,6 +23432,12 @@ var effectsFor = (legacyTarget) => {
|
|
|
23223
23432
|
};
|
|
23224
23433
|
var titleCase = (value) => value.split("-").map((word) => `${word.slice(0, 1).toUpperCase()}${word.slice(1)}`).join(" ");
|
|
23225
23434
|
var summaryFor = (legacyTarget) => {
|
|
23435
|
+
if (legacyTarget === "preview-qbo-customer-invoice") {
|
|
23436
|
+
return "Preview an existing QuickBooks invoice for a BB budget using read-only QBO access. Arguments: <budgetId> <qboInvoiceId>.";
|
|
23437
|
+
}
|
|
23438
|
+
if (legacyTarget === "import-qbo-customer-invoice") {
|
|
23439
|
+
return "Link an existing QuickBooks invoice in BB only after reviewing preview-qbo. Arguments: <budgetId> <qboInvoiceId> --token <previewToken>. Supports closed estimates; no QBO writes or emails.";
|
|
23440
|
+
}
|
|
23226
23441
|
if (legacyTarget === "whoami")
|
|
23227
23442
|
return "Show the active API-key identity.";
|
|
23228
23443
|
if (legacyTarget === "create-bill") {
|
|
@@ -23338,9 +23553,32 @@ var registry2 = [
|
|
|
23338
23553
|
{
|
|
23339
23554
|
name: "--draft-only",
|
|
23340
23555
|
description: "Create a draft for every role; no approval, email, or QuickBooks writes. Conflicts with alreadyPaid: true."
|
|
23556
|
+
},
|
|
23557
|
+
{
|
|
23558
|
+
name: "--quotation-exempt",
|
|
23559
|
+
description: "Explicitly request an eligible server-validated supplier-bill quotation exemption. Cannot be combined with quotation IDs."
|
|
23560
|
+
}
|
|
23561
|
+
]),
|
|
23562
|
+
legacyCommand("validate-bill-selection", ["bill", "selection", "validate"], [], [
|
|
23563
|
+
{
|
|
23564
|
+
name: "--quotationIds <csv>",
|
|
23565
|
+
description: "Approved quotation IDs for a supplier bill. Each selected budget item must be covered once."
|
|
23566
|
+
},
|
|
23567
|
+
{
|
|
23568
|
+
name: "--quotationId <id>",
|
|
23569
|
+
description: "Legacy syntax for selecting one approved quotation."
|
|
23570
|
+
}
|
|
23571
|
+
]),
|
|
23572
|
+
legacyCommand("link-bill-quotations", ["bill", "quotation", "link"], ["billId"], [
|
|
23573
|
+
{
|
|
23574
|
+
name: "--payload <json>",
|
|
23575
|
+
description: "Explicit quotationAllocations for split source coverage on the bill's stored line amounts."
|
|
23576
|
+
},
|
|
23577
|
+
{
|
|
23578
|
+
name: "--quotationIds <csv>",
|
|
23579
|
+
description: "Approved quotation IDs to link across the bill's disjoint line coverage."
|
|
23341
23580
|
}
|
|
23342
23581
|
]),
|
|
23343
|
-
legacyCommand("validate-bill-selection", ["bill", "selection", "validate"]),
|
|
23344
23582
|
legacyCommand("update-bill", ["bill", "update"]),
|
|
23345
23583
|
legacyCommand("update-bill-payment-evidence", [
|
|
23346
23584
|
"bill",
|
|
@@ -23408,6 +23646,14 @@ var registry2 = [
|
|
|
23408
23646
|
"email-context",
|
|
23409
23647
|
"get"
|
|
23410
23648
|
]),
|
|
23649
|
+
legacyCommand("preview-qbo-customer-invoice", [
|
|
23650
|
+
"customer-invoice",
|
|
23651
|
+
"preview-qbo"
|
|
23652
|
+
]),
|
|
23653
|
+
legacyCommand("import-qbo-customer-invoice", [
|
|
23654
|
+
"customer-invoice",
|
|
23655
|
+
"import-qbo"
|
|
23656
|
+
]),
|
|
23411
23657
|
legacyCommand("create-customer-invoice", ["customer-invoice", "create"]),
|
|
23412
23658
|
legacyCommand("discard-customer-invoice", ["customer-invoice", "discard"]),
|
|
23413
23659
|
legacyCommand("delete-customer-invoice", ["customer-invoice", "delete"]),
|
|
@@ -23884,10 +24130,11 @@ Bills
|
|
|
23884
24130
|
list-bills [--projectId] [--budgetId] [--status CSV] [--search <text>] [--isClaimable true|false] [--createdByIds <csv>] [--sortBy createdAt|amount|status] [--sortDir asc|desc] [--page] [--pageSize]
|
|
23885
24131
|
list-claims same flags as list-bills; only reimbursable claims (ignores --isClaimable)
|
|
23886
24132
|
isClaimable differentiates the shared bill/claim records: false = bill, true = claim.
|
|
23887
|
-
|
|
24133
|
+
Create supplier bills from approved quotations from the same supplier and project. Use quotationAllocations plus lineAmounts when quotations split one bill line. Eligible exemptions require explicit --quotation-exempt intent; claims do not consume or link quotations.
|
|
23888
24134
|
create-bill-approval <billId> (also queues approval request emails)
|
|
23889
|
-
create-bill --payload '<json>' [--draft-only] (bill.create; payload.
|
|
23890
|
-
validate-bill-selection --payload '<json>'
|
|
24135
|
+
create-bill --payload '<json>' [--draft-only] [--quotation-exempt] (bill.create; supplier bills use payload.quotationAllocations for split sources or quotationIds for disjoint coverage; explicit allocations require lineAmounts; claims reject quotation sources)
|
|
24136
|
+
validate-bill-selection --payload '<json>' [--quotationIds <csv> | --quotationId <id>] Validate an approved supplier and approved quotation coverage for each supplier-bill line. --quotationId is legacy single-quotation syntax.
|
|
24137
|
+
link-bill-quotations <billId> (--payload '<json>' | --quotationIds <csv>) Admin-only source-link repair; payload.quotationAllocations supports same-line splits.
|
|
23891
24138
|
stage-bill-attachment <projectId> <filePath...> [--file <path>] [--files <csv>] Upload files before create-bill; returns attachment JSON for attachments/paymentProofAttachments
|
|
23892
24139
|
cleanup-staged-bill-attachments <projectId> --keys <csv> Delete unattached staged bill/claim uploads.
|
|
23893
24140
|
update-bill --payload '<json>' (bill.update; must include id)
|
|
@@ -23927,6 +24174,8 @@ Customer invoices
|
|
|
23927
24174
|
No scope flag lists invoices globally. --projectId uses the project scope. A positional budgetId is a backward-compatible shortcut for --budgetIds.
|
|
23928
24175
|
get-customer-invoice <batchId>
|
|
23929
24176
|
get-customer-invoice-email-context <batchId> Contact, required CC, budget, and project context for composing an invoice email.
|
|
24177
|
+
preview-qbo-customer-invoice <budgetId> <qboInvoiceId> Preview linking an existing QBO transaction (numeric ID, not invoice number); works on accepted/closed estimates.
|
|
24178
|
+
import-qbo-customer-invoice <budgetId> <qboInvoiceId> --token <previewToken> Confirm the reviewed BB-only link; requires --allow-state-change --allow-financial-write in automation.
|
|
23930
24179
|
create-customer-invoice --payload '<json>' budgetId + one split with label, percentage, and dueDate.
|
|
23931
24180
|
discard-customer-invoice <batchId> Discard a reserved CREATING batch with no created QBO invoices.
|
|
23932
24181
|
delete-customer-invoice <batchId> Delete QBO invoices and the local batch.
|
|
@@ -24371,7 +24620,7 @@ Waiting for approval\u2026
|
|
|
24371
24620
|
}
|
|
24372
24621
|
configureAccessToken(accessToken);
|
|
24373
24622
|
}
|
|
24374
|
-
if (!resolvedCommand.isLegacyAlias && cmd !== "create-bill" && resolvedCommand.command.effects.length > 0) {
|
|
24623
|
+
if (!resolvedCommand.isLegacyAlias && cmd !== "create-bill" && cmd !== "import-qbo-customer-invoice" && resolvedCommand.command.effects.length > 0) {
|
|
24375
24624
|
await confirmCurrentCommand({
|
|
24376
24625
|
action: resolvedCommand.command.path.join(" "),
|
|
24377
24626
|
target: commandConfirmationTarget(resolvedCommand),
|
|
@@ -24739,7 +24988,11 @@ Waiting for approval\u2026
|
|
|
24739
24988
|
}
|
|
24740
24989
|
const raw = parseJsonFlag(String(payloadRaw), "--payload");
|
|
24741
24990
|
const draftOnly = flags["draft-only"] === true ? true : parseOptionalBoolFlag(flags, "draft-only");
|
|
24742
|
-
|
|
24991
|
+
const quotationExempt = flags["quotation-exempt"] === true;
|
|
24992
|
+
if (flags["quotation-exempt"] !== undefined && flags["quotation-exempt"] !== true) {
|
|
24993
|
+
throw new Error("--quotation-exempt does not take a value.");
|
|
24994
|
+
}
|
|
24995
|
+
await createBillFromPayload(raw, draftOnly, quotationExempt);
|
|
24743
24996
|
break;
|
|
24744
24997
|
}
|
|
24745
24998
|
case "validate-bill-selection": {
|
|
@@ -24748,7 +25001,37 @@ Waiting for approval\u2026
|
|
|
24748
25001
|
throw new Error("validate-bill-selection requires --payload '<json>'");
|
|
24749
25002
|
}
|
|
24750
25003
|
const raw = parseJsonFlag(String(payloadRaw), "--payload");
|
|
24751
|
-
|
|
25004
|
+
if (flags.quotationIds === true || flags.quotationId === true) {
|
|
25005
|
+
throw new Error("--quotationIds requires a CSV value and --quotationId requires an ID value.");
|
|
25006
|
+
}
|
|
25007
|
+
const quotationIds = parseCommaSeparatedIds(getFlag(flags, "quotationIds"));
|
|
25008
|
+
const quotationId = getFlag(flags, "quotationId")?.trim() || undefined;
|
|
25009
|
+
if (quotationId && quotationIds && (quotationIds.length !== 1 || quotationIds[0] !== quotationId)) {
|
|
25010
|
+
throw new Error("--quotationId conflicts with --quotationIds.");
|
|
25011
|
+
}
|
|
25012
|
+
if (quotationIds && new Set(quotationIds).size !== quotationIds.length) {
|
|
25013
|
+
throw new Error("--quotationIds must not contain duplicates.");
|
|
25014
|
+
}
|
|
25015
|
+
await validateBillSelectionFromPayload(raw, {
|
|
25016
|
+
...quotationId ? { quotationId } : {},
|
|
25017
|
+
...quotationIds ? { quotationIds } : {}
|
|
25018
|
+
});
|
|
25019
|
+
break;
|
|
25020
|
+
}
|
|
25021
|
+
case "link-bill-quotations": {
|
|
25022
|
+
const billId = positional[0];
|
|
25023
|
+
const payloadRaw = getFlag(flags, "payload");
|
|
25024
|
+
const quotationIds = parseCommaSeparatedIds(getFlag(flags, "quotationIds"));
|
|
25025
|
+
if (!billId || !payloadRaw && (!quotationIds || quotationIds.length === 0)) {
|
|
25026
|
+
throw new Error("link-bill-quotations requires <billId> and either --payload '<json>' or --quotationIds <csv>");
|
|
25027
|
+
}
|
|
25028
|
+
if (payloadRaw && quotationIds) {
|
|
25029
|
+
throw new Error("--payload cannot be combined with --quotationIds.");
|
|
25030
|
+
}
|
|
25031
|
+
if (quotationIds && new Set(quotationIds).size !== quotationIds.length) {
|
|
25032
|
+
throw new Error("--quotationIds must not contain duplicates.");
|
|
25033
|
+
}
|
|
25034
|
+
await linkBillQuotations(billId, payloadRaw ? parseJsonFlag(String(payloadRaw), "--payload") : { quotationIds });
|
|
24752
25035
|
break;
|
|
24753
25036
|
}
|
|
24754
25037
|
case "update-bill": {
|
|
@@ -25123,6 +25406,26 @@ Waiting for approval\u2026
|
|
|
25123
25406
|
await getCustomerInvoiceEmailContext(batchId);
|
|
25124
25407
|
break;
|
|
25125
25408
|
}
|
|
25409
|
+
case "preview-qbo-customer-invoice":
|
|
25410
|
+
case "import-qbo-customer-invoice": {
|
|
25411
|
+
const [budgetId, qboInvoiceId] = positional;
|
|
25412
|
+
if (!budgetId?.trim() || !qboInvoiceId || positional.length !== 2) {
|
|
25413
|
+
throw new Error(`${cmd} requires <budgetId> <qboInvoiceId>`);
|
|
25414
|
+
}
|
|
25415
|
+
if (!/^\d+$/.test(qboInvoiceId)) {
|
|
25416
|
+
throw new Error("qboInvoiceId must be the numeric QuickBooks transaction ID, not the invoice number");
|
|
25417
|
+
}
|
|
25418
|
+
if (cmd === "preview-qbo-customer-invoice") {
|
|
25419
|
+
await previewQboCustomerInvoice({ budgetId, qboInvoiceId });
|
|
25420
|
+
} else {
|
|
25421
|
+
const token = getFlag(flags, "token");
|
|
25422
|
+
if (!token || !/^[a-f0-9]{64}$/.test(token)) {
|
|
25423
|
+
throw new Error("import-qbo-customer-invoice requires --token <64-character preview token> from a reviewed preview-qbo result");
|
|
25424
|
+
}
|
|
25425
|
+
await importQboCustomerInvoice({ budgetId, qboInvoiceId, token });
|
|
25426
|
+
}
|
|
25427
|
+
break;
|
|
25428
|
+
}
|
|
25126
25429
|
case "create-customer-invoice": {
|
|
25127
25430
|
const payloadRaw = getFlag(flags, "payload");
|
|
25128
25431
|
if (!payloadRaw) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@go-labs-sg/bb",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.32.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",
|
|
@@ -129,6 +129,7 @@ The following distinctions are especially important:
|
|
|
129
129
|
- Project Hub setup and sync are asynchronous external writes. Read `bb project hub status <project-id>` first, obtain explicit approval, and request the mutation once instead of repeatedly queueing it. Setup readiness remains visible through `status`; commercial sync returns an outbox operation ID for Admin diagnostics.
|
|
130
130
|
- Bill/claim status commands have separate role rules for `CHECKED`, `APPROVED`, and `PAID`.
|
|
131
131
|
- `bb bill create --payload` uses `isClaimable=false`; `isClaimable=true` creates a claim. Claims intentionally permit some base-user flows that supplier bills do not.
|
|
132
|
+
- Create supplier bills from approved quotations by passing `quotationIds` for disjoint coverage, or `quotationAllocations` plus exact `lineAmounts` when quotations split a bill line. Use `--quotation-exempt` only for an existing server-validated exemption. Claims share budget allowance but never consume or link quotations, and reject quotation source fields.
|
|
132
133
|
- For a draft-only bill or claim, use `bb bill create --draft-only --payload` (or `draftOnly: true` in the payload). This forces `DRAFT` for every role and requires only `--allow-state-change --allow-financial-write`. It sends no approval/email and performs no QuickBooks writes. Do not combine it with `alreadyPaid: true`. An older API without `bill.createDraft` rejects the command; never retry with ordinary creation to bypass that error.
|
|
133
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.
|
|
134
135
|
- Staged attachments belong to the user in the database. Do not reuse, share, or manually construct staged keys.
|