@go-labs-sg/bb 2.31.1 → 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 +17 -3
- package/command-manifest.json +70 -0
- package/command-reference.md +8 -0
- package/dist/index.js +270 -17
- 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.
|
|
@@ -315,7 +327,7 @@ Bill, claim, and quotation mutations accept only suppliers whose approval status
|
|
|
315
327
|
| Area | Legacy handler labels (reference only; non-exhaustive) |
|
|
316
328
|
| --- | --- |
|
|
317
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`) |
|
|
318
|
-
| **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` |
|
|
319
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`) |
|
|
320
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` |
|
|
321
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` |
|
|
@@ -391,5 +403,7 @@ relationships or allocations block approval and require review. Multi-line quota
|
|
|
391
403
|
create/update payloads must include `lineAmounts` with `{ budgetItemId, amount }`
|
|
392
404
|
for every selected line, summing to `amountWithoutGst`. The server revalidates
|
|
393
405
|
existing quotation links, reviewed usage, and per-line allowance inside the
|
|
394
|
-
approval transaction.
|
|
395
|
-
|
|
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"],
|
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.
|
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}`,
|
|
@@ -22250,11 +22438,16 @@ async function updateBillFromPayload(raw) {
|
|
|
22250
22438
|
const result = await api2.bill.update.mutate(input2);
|
|
22251
22439
|
out(result);
|
|
22252
22440
|
}
|
|
22253
|
-
var validateBillSelectionFromPayload = async (raw) => {
|
|
22254
|
-
const input2 = parseValidateBillSelectionPayload(raw);
|
|
22441
|
+
var validateBillSelectionFromPayload = async (raw, quotationFlags) => {
|
|
22442
|
+
const input2 = parseValidateBillSelectionPayload(raw, quotationFlags);
|
|
22255
22443
|
const result = await api2.bill.validateSelection.mutate(input2);
|
|
22256
22444
|
out(result);
|
|
22257
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
|
+
};
|
|
22258
22451
|
var updateBillPaymentEvidenceFromPayload = async (raw) => {
|
|
22259
22452
|
const input2 = parseUpdateBillPaymentEvidencePayload(raw);
|
|
22260
22453
|
const result = await api2.bill.updatePaymentEvidence.mutate(input2);
|
|
@@ -23092,6 +23285,7 @@ var mutationPrefixes = [
|
|
|
23092
23285
|
"delete-",
|
|
23093
23286
|
"discard-",
|
|
23094
23287
|
"import-",
|
|
23288
|
+
"link-",
|
|
23095
23289
|
"mark-",
|
|
23096
23290
|
"patch-",
|
|
23097
23291
|
"reactivate-",
|
|
@@ -23180,6 +23374,7 @@ var externalWriteTargets = new Set([
|
|
|
23180
23374
|
]);
|
|
23181
23375
|
var financialWriteTargets = new Set([
|
|
23182
23376
|
"import-qbo-customer-invoice",
|
|
23377
|
+
"link-bill-quotations",
|
|
23183
23378
|
"approve-bill",
|
|
23184
23379
|
"approve-customer-invoice",
|
|
23185
23380
|
"complete-project",
|
|
@@ -23358,9 +23553,32 @@ var registry2 = [
|
|
|
23358
23553
|
{
|
|
23359
23554
|
name: "--draft-only",
|
|
23360
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."
|
|
23361
23580
|
}
|
|
23362
23581
|
]),
|
|
23363
|
-
legacyCommand("validate-bill-selection", ["bill", "selection", "validate"]),
|
|
23364
23582
|
legacyCommand("update-bill", ["bill", "update"]),
|
|
23365
23583
|
legacyCommand("update-bill-payment-evidence", [
|
|
23366
23584
|
"bill",
|
|
@@ -23912,10 +24130,11 @@ Bills
|
|
|
23912
24130
|
list-bills [--projectId] [--budgetId] [--status CSV] [--search <text>] [--isClaimable true|false] [--createdByIds <csv>] [--sortBy createdAt|amount|status] [--sortDir asc|desc] [--page] [--pageSize]
|
|
23913
24131
|
list-claims same flags as list-bills; only reimbursable claims (ignores --isClaimable)
|
|
23914
24132
|
isClaimable differentiates the shared bill/claim records: false = bill, true = claim.
|
|
23915
|
-
|
|
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.
|
|
23916
24134
|
create-bill-approval <billId> (also queues approval request emails)
|
|
23917
|
-
create-bill --payload '<json>' [--draft-only] (bill.create; payload.
|
|
23918
|
-
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.
|
|
23919
24138
|
stage-bill-attachment <projectId> <filePath...> [--file <path>] [--files <csv>] Upload files before create-bill; returns attachment JSON for attachments/paymentProofAttachments
|
|
23920
24139
|
cleanup-staged-bill-attachments <projectId> --keys <csv> Delete unattached staged bill/claim uploads.
|
|
23921
24140
|
update-bill --payload '<json>' (bill.update; must include id)
|
|
@@ -24769,7 +24988,11 @@ Waiting for approval\u2026
|
|
|
24769
24988
|
}
|
|
24770
24989
|
const raw = parseJsonFlag(String(payloadRaw), "--payload");
|
|
24771
24990
|
const draftOnly = flags["draft-only"] === true ? true : parseOptionalBoolFlag(flags, "draft-only");
|
|
24772
|
-
|
|
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);
|
|
24773
24996
|
break;
|
|
24774
24997
|
}
|
|
24775
24998
|
case "validate-bill-selection": {
|
|
@@ -24778,7 +25001,37 @@ Waiting for approval\u2026
|
|
|
24778
25001
|
throw new Error("validate-bill-selection requires --payload '<json>'");
|
|
24779
25002
|
}
|
|
24780
25003
|
const raw = parseJsonFlag(String(payloadRaw), "--payload");
|
|
24781
|
-
|
|
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 });
|
|
24782
25035
|
break;
|
|
24783
25036
|
}
|
|
24784
25037
|
case "update-bill": {
|
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.
|